Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RF: Configuration module #5

Merged
merged 5 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: install-test

on:
push:
branches:
- master
tags:
- '*'
pull_request:
branches:
- master
schedule:
# 7am EST / 8am EDT Mondays
- cron: '0 12 * * 1'

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, 3.10]
install: [repo]
pip-flags: ['', '--pre']
include:
- python-version: 3.9
install: sdist
pip-flags: ''
- python-version: 3.9
install: wheel
pip-flags: ''
- python-version: 3.9
install: editable
pip-flags: ''

env:
INSTALL_TYPE: ${{ matrix.install }}
PIP_FLAGS: ${{ matrix.pip-flags }}

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Select archive
run: |
if [ "$INSTALL_TYPE" = "sdist" ]; then
ARCHIVE=$( ls dist/*.tar.gz )
elif [ "$INSTALL_TYPE" = "wheel" ]; then
ARCHIVE=$( ls dist/*.whl )
elif [ "$INSTALL_TYPE" = "repo" ]; then
ARCHIVE="."
elif [ "$INSTALL_TYPE" = "editable" ]; then
ARCHIVE="-e ."
fi
echo "ARCHIVE=$ARCHIVE" >> $GITHUB_ENV
- name: Install package and test dependencies
run: python -m pip install $PIP_FLAGS $ARCHIVE[test]
- name: Run tests
run: python -m pytest -sv --doctest-modules etelemetry
105 changes: 82 additions & 23 deletions etelemetry/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,77 @@


DEFAULT_ENDPOINT = "http://0.0.0.0:8000/graphql" # localhost test
CONFIG_FILENAME = Path.home() / '.cache' / 'etelemetry' / 'config.json'
DEFAULT_CONFIG_FILE = Path.home() / '.cache' / 'etelemetry' / 'config.json'

# TODO: 3.10 - Replace with | operator
File = typing.Union[str, Path]


@dataclass
class Config:
"""
Class to store client-side configuration, facilitating communication with the server.

The class stores the following components:
- `endpoint`:
The URL of the etelemetry server
- `user_id`:
A string representation of a UUID (RFC 4122) assigned to the user.
- `session_id`:
A string representation of a UUID assigned to the lifespan of the etelemetry invocation.
"""
endpoint: str = None
user_id: uuid.UUID = None
user_id: str = None
session_id: str = None
_is_setup = False


def load(filename: File = CONFIG_FILENAME) -> bool:
@classmethod
def init(
cls,
*,
endpoint: str = None,
user_id: str = None,
session_id: str = None,
final: bool = True,
) -> None:
if cls._is_setup:
return
if endpoint is not None:
cls.endpoint = endpoint
elif cls.endpoint is None:
cls.endpoint = DEFAULT_ENDPOINT
if user_id is not None or cls.user_id is None:
try:
uuid.UUID(user_id)
cls.user_id = user_id
except Exception:
cls.user_id = gen_uuid()
# Do not set automatically, leave to developers
if session_id is not None:
try:
uuid.UUID(session_id)
cls.session_id = session_id
except Exception:
pass
cls._is_setup = final


@classmethod
def _reset(cls):
cls.endpoint = None
cls.user_id = None
cls.session_id = None
cls._is_setup = False


def load(filename: File) -> bool:
"""Load existing configuration file, or create a new one."""
config = json.loads(Path(filename).read_text())
Config.endpoint = config.get("endpoint")
user_id = config.get("user_id")
if user_id:
Config.user_id = uuid.UUID(user_id)
Config._is_setup = True
Config.init(final=False, **config)
return True


def save(filename: File = CONFIG_FILENAME) -> str:
def save(filename: File) -> str:
"""Save to a file."""
config = {
field: getattr(Config, field) for field in Config.__annotations__.keys()
Expand All @@ -42,23 +88,36 @@ def save(filename: File = CONFIG_FILENAME) -> str:
return str(filename)


def setup(et_endpoint: str = None, user_id: uuid.UUID = None, filename: File = CONFIG_FILENAME):
"""Configure the client, and save configuration to an output file."""
def setup(
*,
endpoint: str = None,
user_id: str = None,
session_id: str = None,
save_config: bool = True,
filename: File = None,
) -> None:
"""
Configure the client, and save configuration to an output file.

This method is invoked before each API call, but can also be called by
application developers for finer-grain control.
"""
if Config._is_setup:
return
filename = filename or DEFAULT_CONFIG_FILE
if Path(filename).exists():
return load(filename)
Config.endpoint = et_endpoint or DEFAULT_ENDPOINT
Config.user_id = user_id or gen_user_uuid()
Config._is_setup = True
save(filename)
load(filename)
# if any parameters have been set, override the current attribute
Config.init(endpoint=endpoint, user_id=user_id, session_id=session_id)
if save_config:
save(filename)


def gen_user_uuid(uuid_factory: str = "safe") -> uuid.UUID:
def gen_uuid(uuid_factory: str = "safe") -> str:
"""
Generate a user ID in UUID format.
Generate a RFC 4122 UUID.

Depending on what `uuid_factory` is provided, the user ID will be generated differently:
Depending on what `uuid_factory` is provided, the UUID will be generated differently:
- `safe`: This is multiprocessing safe, and uses system information.
- `random`: This is random, and may run into problems if setup is called across multiple
processes.
Expand All @@ -70,12 +129,12 @@ def gen_user_uuid(uuid_factory: str = "safe") -> uuid.UUID:
# TODO: 3.10 - Replace with match/case
if uuid_factory == "safe":
return _safe_uuid_factory()
if uuid_factory == "random":
return uuid.uuid4()
elif uuid_factory == "random":
return str(uuid.uuid4())
raise NotImplementedError


def _safe_uuid_factory() -> uuid.UUID:
def _safe_uuid_factory() -> str:
import getpass
import socket

Expand Down
Empty file added etelemetry/tests/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions etelemetry/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import uuid

import pytest

from .. import config


@pytest.fixture(autouse=True)
def tmp_config(tmp_path, monkeypatch):
home = tmp_path / 'config.json'
monkeypatch.setattr(config, 'DEFAULT_CONFIG_FILE', home)


def test_setup_default():

conf = config.Config
assert conf.endpoint is None
assert conf.user_id is None
assert conf.session_id is None
assert conf._is_setup is False

config.setup()
assert conf.endpoint == config.DEFAULT_ENDPOINT
assert uuid.UUID(conf.user_id)
assert conf.session_id is None
assert conf._is_setup is True

# after being set up, cannot be overriden
new_endpoint = 'https://github.com'
config.setup(endpoint=new_endpoint)
assert conf.endpoint == config.DEFAULT_ENDPOINT

# but fine if cleared
conf._reset()
assert conf.endpoint is None
config.setup(endpoint=new_endpoint)
assert conf.endpoint == new_endpoint
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ install_requires =
ci-info >= 0.2
typing_extensions; python_version<'3.8'

[options.extras_require]
test =
pytest

[flake8]
max-line-length = 99

Expand Down