diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b136fbb --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: unit tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-20.04 + + strategy: + matrix: + python-version: [3.9.2] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install --with test + - name: Run tests + run: | + poetry run pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afadc2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python bytecode +__pycache__/ +.py[cod] + +# Environments +venv + +# Coverage reports +.coverage +*.xml + +# mypy +.mypy_cache/ + +# Editors +*~ +.vscode diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5a690e --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +[![Python package](https://github.com/MPI-IS/nightskycam-serialization/actions/workflows/tests.yml/badge.svg)](https://github.com/MPI-IS/nightskycam-serialization/actions/workflows/tests.yml) +[![PyPI version](https://img.shields.io/pypi/v/nightskycam-serialization.svg)](https://pypi.org/project/nightskycam-serialization/) + + +> 🚧 **Under Construction** +> This project is currently under development. Please check back later for updates. + + +# Nightskycam Serialization + +## About + +This is is a support package for: + +- [nightskycam](https://gitlab.is.tue.mpg.de/allsky/nightskycam) +- [nightskycam-server](https://gitlab.is.tue.mpg.de/allsky/nightskycam-server) + +These two packages do not depend on each other, yet the services they spawn will communicate with each other in the form of +serialized messages (sent via websockets). This requires the code in these two packages to follow the same convention on how +messages are serialized and deserialized. To enforce this, these two packages are dependant on the nightskycam-serialization +package and use its API for serializing / deserializing messages. + +## Getting Started as a User (using `pip`) + +Dependency management with `pip` is easier to set up than with `poetry`, but the optional dependency-groups are not installable with `pip`. + +* Create and activate a new Python virtual environment: + ```bash + python3 -m venv --copies venv + source venv/bin/activate + ``` +* Update `pip` and build package: + ```bash + pip install -U pip # optional but always advised + pip install . # -e option for editable mode + ``` + +## Getting Started as a Developer (using `poetry`) + +Dependency management with `poetry` is required for the installation of the optional dependency-groups. + +* Install [poetry](https://python-poetry.org/docs/). +* Install dependencies for package + (also automatically creates project's virtual environment): + ```bash + poetry install + ``` +* Install `dev` dependency group: + ```bash + poetry install --with dev + ``` +* Activate project's virtual environment: + ```bash + poetry shell + ``` + +## Tests (only possible for setup with `poetry`, not with `pip`) + + +To install `test` dependency group: +```bash +poetry install --with test +``` + +To run the tests: +```bash +python -m pytest tests +``` + +To extract coverage data: +* Get code coverage by measuring how much of the code is executed when running the tests: + ```bash + coverage run -m pytest tests + ``` +* View coverage results: + ```bash + # Option 1: simple report in terminal. + coverage report + # Option 2: nicer HTML report. + coverage html # Open resulting 'htmlcov/index.html' in browser. + ``` diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..95d91f3 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] + +[mypy-tomli_w.*] +ignore_missing_imports = True + +[mypy-pytest.*] +ignore_missing_imports = True diff --git a/nightskycam_serialization/__init__.py b/nightskycam_serialization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nightskycam_serialization/command.py b/nightskycam_serialization/command.py new file mode 100644 index 0000000..eae8e70 --- /dev/null +++ b/nightskycam_serialization/command.py @@ -0,0 +1,124 @@ +from dataclasses import asdict, dataclass +from typing import Dict, Optional, Tuple, Union + +from .serialize import ImproperMessage, deserialize, serialize + + +def serialize_command( + command_id: int, command: str, token: Optional[str] = None +) -> str: + """ + Serialize a command to be executed on the system + + Counterpart, see: [deserialize_command](). + + Arguments + command_id: unique identifier of the command + command: the command to execute + token: see [serialize.serialize]() + + Returns + The serialized message + """ + d: Dict[str, Union[int, str]] = { + "command_id": command_id, + "command": command, + } + return serialize(d, token=token) + + +def deserialize_command(message: str, token: Optional[str] = None) -> Tuple[int, str]: + """ + Deserialize a message sent by the server into a command to + be executed by the system. + + Counterpart, see [serialize_command]() + + Arguments + message: the serialized message + token: see [serialize.deserialize]() + + Returns + command_id: unique identifier of the command + command: the command to execute + + + Raises + [improper_message.ImproperMessage]() if message is not properly + formated. + """ + + data = deserialize( + message, + required_keys=("command_id", "command"), + token=token, + ) + try: + command_id = int(data["command_id"]) + except ValueError: + raise ImproperMessage( + "value for key 'command_id' should be an int, " + f"but received value {data['command_id']} of type {type(data['command_id'])} instead." + ) + + return command_id, str(data["command"]) + + +@dataclass +class CommandResult: + """ + Summary of the "output" of a command executed on the system. + """ + + command_id: int + command: str + stdout: str = "" + stderr: str = "" + exit_code: str = "" + error: str = "" + + +def serialize_command_result(result: CommandResult, token: Optional[str] = None) -> str: + """ + Serialize a command output to a string. + + Counterpart method: [deserialize_command_result](). + + Arguments + result: output of the command + token: see [serialize.serialize]() + + Returns + serialized result + """ + data = asdict(result) + return serialize(data, token=token) + + +def deserialize_command_result( + message: str, token: Optional[str] = None +) -> CommandResult: + """ + Deserialize a message into an instance of CommandResult + + Counterpart method: [serialize_command_result](). + + Arguments: + message: the serialized message + token: see [serialize.deserialize]() + + Returns + The corresponding instance of CommandResult + + Raises + [improper_message.ImproperMessage]() if message is not properly formatted. + """ + + data = deserialize(message, required_keys=tuple(), token=token) + try: + return CommandResult(**data) + except TypeError as te: + raise ImproperMessage( + "failed to deserialize the message into an instance " + f"of CommandResult: {te}" + ) diff --git a/nightskycam_serialization/config.py b/nightskycam_serialization/config.py new file mode 100644 index 0000000..c58b8f8 --- /dev/null +++ b/nightskycam_serialization/config.py @@ -0,0 +1,65 @@ +from typing import Any, Dict, Optional, Tuple + +from .fix import deserialize_fix_dict, serialize_fix_dict +from .serialize import ImproperMessage, deserialize, serialize + +_None = "__None__" + + +def serialize_config_update( + runner_name: str, config: Dict[str, Any], token: Optional[str] = None +) -> str: + """ + For server to request runners of remote system to reconfigure + themselves. + + Counterpart: [deserialize_config_update]() + + Arguments + runner_name: name of the runner that needs reconfiguring + config: the new configuration to apply. + token: see [serialize.serialize]() + + Returns + corresponding serialized message + """ + + d = {"runner_name": runner_name, "config": serialize_fix_dict(config)} + return serialize(d, token=token) + + +def _deserialize_fix(value: Any) -> Any: + # Casting back "__None__" to None. + if isinstance(value, str) and value == "__None__": + return None + return value + + +def _deserialize_fix_config(config: Dict[str, Any]) -> Dict[str, Any]: + return {k: _deserialize_fix(v) for k, v in config.items()} + + +def deserialize_config_update( + message: str, token: Optional[str] = None +) -> Tuple[str, Dict[str, Any]]: + """ + For system receiving configuration update request for a runner. + + Counterpart: [serialize_config_update]() + + Arguments + message: the serialized request + token: see [serialize.deserialize]() + + Returns + Tuple: + - the name of the runner that should update + - the configuration + """ + + data = deserialize(message, required_keys=("runner_name", "config"), token=token) + if not isinstance(data["config"], dict): + raise ImproperMessage( + f"configuration expected to be a dictionary, but got {data['config']} of type {type(data['config'])} instead." + ) + return (str(data["runner_name"]), deserialize_fix_dict(data["config"])) diff --git a/nightskycam_serialization/fix.py b/nightskycam_serialization/fix.py new file mode 100644 index 0000000..0cf8dc9 --- /dev/null +++ b/nightskycam_serialization/fix.py @@ -0,0 +1,62 @@ +from pathlib import Path +from typing import Any, Dict, TypeVar + +from nightskyrunner.status import StatusDict + +_None = "__None__" + + +SerialDict = TypeVar("SerialDict", Dict[str, Any], StatusDict) + + +def serialize_fix(value: Any) -> Any: + """ + Sometimes python dictionary can not be serialized into + toml. Here fixing the commonly encountered issue: + + - pathlib.Path (cast to string) + - None (cast to the string "__None__") + + See: [deserialize_fix]() + """ + + if value is None: + return _None + if isinstance(value, Path): + return str(value) + return value + + +def serialize_fix_dict(config: SerialDict) -> SerialDict: + """ + Applying [serialize_fix] to all values of the dictionary + + See: [deserialize_fix_dict]() + """ + instance = type(config)() + for k, v in config.items(): + instance[k] = serialize_fix(v) # type: ignore + return instance + + +def deserialize_fix(value: Any) -> Any: + """ + Casting the string "__None__" to None. + + See: [serialize_fix]() + """ + + # Casting back "__None__" to None. + if isinstance(value, str) and value == "__None__": + return None + return value + + +def deserialize_fix_dict(config: Dict[str, Any]) -> Dict[str, Any]: + """ + Applying [deserialize_fix]() to all values of the dictionary. + + See: [serialize_fix_dict](). + """ + + return {k: deserialize_fix(v) for k, v in config.items()} diff --git a/nightskycam_serialization/py.typed b/nightskycam_serialization/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/nightskycam_serialization/serialize.py b/nightskycam_serialization/serialize.py new file mode 100644 index 0000000..2ee3dcd --- /dev/null +++ b/nightskycam_serialization/serialize.py @@ -0,0 +1,79 @@ +from typing import Any, Dict, Iterable, Optional + +import tomli +import tomli_w + + +class ImproperMessage(Exception): + ... + + +class IncorrectToken(Exception): + ... + + +_token_key: str = "__token__" + + +def _check_keys(required_keys: Iterable[str], d: Dict[str, Any]) -> None: + """ + Raises an [improper_message.ImproperMessage]() error if any + key is missing from d. + """ + + missing_keys = [rk for rk in required_keys if rk not in d.keys()] + if missing_keys: + raise ImproperMessage(f"missing key(s): {', '.join(missing_keys)}") + + +def serialize(d: Dict[str, Any], token: Optional[str] = None) -> str: + """ + Cast d to a string. + Counterpart: [deserialize]() + + If 'token' is not None, its value will be added to the dictionary + under the key '__token__'. If this key already exists in d, a + ValueError will be raised. + """ + if token: + if _token_key in d: + raise ValueError( + f"Can not serialize a dictionary with key '{_token_key}'" + "(key reserved by nightskycam-serialization)" + ) + d[_token_key] = token + return tomli_w.dumps(d) + + +def deserialize( + message: str, + required_keys: Iterable = tuple(), + token: Optional[str] = None, +) -> Dict[str, Any]: + """ + Cast message to a dictionary. + Counterpart: [serialize](). + + Arguments + message: the message to deserialize + required_keys: if any of the keys is missing from the deserialized dictionary, + an ImproperMessage error is raised + token: if not None, the deserialized dictionary is expected to have a '__token__' + key associated with this value. If not, an IncorrectToken error is raised. + """ + data = tomli.loads(message) + if token: + if _token_key not in data: + raise IncorrectToken( + f"The key {_token_key} was expected in the message, but is missing" + ) + if data[_token_key] != token: + raise IncorrectToken( + "Received message contains the token " + f"{data[_token_key]}, " + f"which does not match the specified token {token}." + ) + del data[_token_key] + if required_keys: + _check_keys(required_keys, data) + return data diff --git a/nightskycam_serialization/status.py b/nightskycam_serialization/status.py new file mode 100644 index 0000000..1d5af6f --- /dev/null +++ b/nightskycam_serialization/status.py @@ -0,0 +1,617 @@ +from contextlib import suppress +from enum import Enum +from inspect import isfunction +import random +import string +import typing +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Literal, + Optional, + Tuple, + Type, + TypedDict, + Union, + cast, +) + +from nightskyrunner.status import RunnerStatusDict, StatusDict + +from .fix import deserialize_fix_dict, serialize_fix_dict +from .serialize import deserialize, serialize + +# --- dev notes --- +# +# The following conventions have to be enforced: +# Subclasses of RunnerStatusDict *must* be named +# {runner class name}Entries (e.g. CommandRunnerEntries) +# +# report class (which "cast" an instance of RunnerStatusDict +# to Dict[str,str], *must* be named: +# get_{runner class name}_report +# + + +NightskycamRunner = Literal[ + "CamRunner", + "AsiCamRunner", + "USBCamRunner", + "ImageProcessRunner", + "SpaceKeeperRunner", + "FtpRunner", + "LocationInfoRunner", + "SleepyPiRunner", + "CommandRunner", + "StatusRunner", + "ConfigRunner", +] +"""The list of runner classes defined and used by nightskycam""" + + +class RunnerClasses(Enum): + """ + Enumerating all known runner classes + """ + + # dev note: there is a unit test checking + # there is no mismatch with NightskycamRunner + # right above + CamRunner = "CamRunner" + AsiCamRunner = "AsiCamRunner" + USBCamRunner = "USBCamRunner" + CommandRunner = "CommandRunner" + FtpRunner = "FtpRunner" + LocationInfoRunner = "LocationInfoRunner" + ImageProcessRunner = "ImageProcessRunner" + SleepyPiRunner = "SleepyPiRunner" + SpaceKeeperRunner = "SpaceKeeperRunner" + ConfigRunner = "ConfigRunner" + StatusRunner = "StatusRunner" + + +def serialize_status( + system: str, status: Iterable[StatusDict], token: Optional[str] = None +) -> str: + """ + Serialize the dictionary with the keys "system" and "status". + + Arguments + system: the name of the nightskycam system + status: the status dictionary to serialize + token: the shared secret + + Counterpart: [deserialize_status](). + + See: [serialize.serialize]() + """ + d: Dict[str, Union[str, Dict[str, StatusDict]]] = {"system": system} + status_d: Dict[str, StatusDict] = { + s["name"]: serialize_fix_dict(s) for s in status + } + d["status"] = status_d + return serialize(d, token=token) + + +def deserialize_status( + message: str, token: Optional[str] = None +) -> Tuple[str, Dict[str, Dict[str, Any]]]: + """ + Deserialize the message sent by a system, containing the status information + related to all runners executed on the system. + + Returns: + The name of the system + The status dictionary for each runner (name of the runner as key) + + Counterpart: [serialize_status](). + + See: [serialize.deserialize]() + """ + d = deserialize(message, required_keys=("system", "status"), token=token) + system = d["system"] + all_status = { + runner_name: deserialize_fix_dict(status) + for runner_name, status in d["status"].items() + } + return system, all_status + + +class CamRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam CamRunner + """ + + time_window: str + use_sun_alt: bool + use_weather: bool + active: str + picture: str + number_of_pictures_taken: int + latest_picture: str + camera_info: Dict[str, str] + pause: bool + + +class AsiCamRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam AsiCamRunner + """ + + time_window: str + use_sun_alt: bool + use_weather: bool + active: str + picture: str + number_of_pictures_taken: int + latest_picture: str + camera_info: Dict[str, str] + pause: bool + + +class USBCamRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam USBCamRunner + """ + + time_window: str + use_sun_alt: bool + use_weather: bool + active: str + picture: str + number_of_pictures_taken: int + latest_picture: str + camera_info: Dict[str, str] + pause: bool + + +def get_CamRunner_report( + sc: CamRunnerEntries, camera_type: str = "camera" +) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + entries: Dict[str, str] = {} + + pause = sc.get("pause", False) + if pause: + return {camera_type: "paused - visit 'snapshots' to reactivate"} + + entries["active"] = sc.get("active", None) + + usa = sc.get("use_sun_alt", None) + entries["use sun altitude"] = "yes" if usa else "no" + + uw = sc.get("use_weather", None) + entries["use weather"] = "yes" if uw else "no" + + entries["camera temperature"] = sc.get("camera_info", {}).get( + "camera_temperature", None + ) + + entries["number of pictures taken (since boot)"] = sc.get( + "number_of_pictures_taken", None + ) + + r = "\n".join(f"{k}: {v}" for k, v in entries.items() if v is not None) + + return {camera_type: r} + + +def get_AsiCamRunner_report(sc: AsiCamRunnerEntries) -> Dict[str, str]: + return get_CamRunner_report(sc, "ASI camera") + + +def get_USBCamRunner_report(sc: USBCamRunnerEntries) -> Dict[str, str]: + return get_CamRunner_report(sc, "USB camera") + + +class CommandRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam CommandRunner + """ + + queued_commands: List[int] + active_command: str + executed_commands: List[int] + + +def get_CommandRunner_report(sc: CommandRunnerEntries) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + try: + return {"running command": str(sc["active_command"])} + except KeyError: + return {} + + +class FtpRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam FtpRunner + """ + + uploading: bool + number_uploaded_files: str + total_uploaded_files: str + upload_speed: float + files_to_upload: str + latest_uploaded: str + + +def get_FtpRunner_report(sf: FtpRunnerEntries) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + ftp_infos: List[str] = [] + with suppress(KeyError): + uploading = sf["uploading"] + if uploading: + ftp_infos.append("currently uploading: yes") + else: + ftp_infos.append("currently uploading: no") + with suppress(KeyError): + ftp_infos.append( + str( + f"uploaded files: " + f"{sf['number_uploaded_files']} file(s)" + f" ({sf['total_uploaded_files']})" + ) + ) + with suppress(KeyError): + ftu = sf["files_to_upload"] + if ftu: + ftp_infos.append("files to upload: " + str(ftu)) + else: + ftp_infos.append("no file to upload") + if not ftp_infos: + return {} + return {"ftp": "\n".join(ftp_infos)} + + +class LocationInfoRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam LocationInfoRunner + """ + + latitude: float + longitude: float + name: str + country: str + timezone: str + IPs: str + local_time: str + sun_alt: float + sun_alt_threshold: float + night: bool + cloud_cover: int + weather: str + temperature: int + time_stamp: float + cpu_temperature: int + + +def get_LocationInfoRunner_report( + li: LocationInfoRunnerEntries, +) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + + r: Dict[str, str] = {} + local_info: Dict[str, str] = {} + with suppress(KeyError): + local_info["local time"] = li["local_time"] + with suppress(KeyError): + local_info["sun altitude"] = str( + f"{li['sun_alt']:.2f} " + f"(threshold: {li['sun_alt_threshold']:.2f})" + ) + with suppress(KeyError): + local_info["weather"] = str( + f"{li['weather']} (cloud cover: {li['cloud_cover']})" + ) + if local_info: + r["local info"] = "\n".join( + [f"{k}: {v}" for k, v in local_info.items()] + ) + with suppress(KeyError): + r["IP(s)"] = li["IPs"] + + return r + + +class ImageProcessRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam ProcessRunner + """ + + number_of_processed_pictures: int + processes_applied: List[str] + file_format: str + last_processed_picture: str + + +def get_ImageProcessRunner_report( + sp: ImageProcessRunnerEntries, +) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + processes_applied = sp.get("processes_applied", "") + processes_applied_str = ( + f"{processes_applied}, " if processes_applied else "" + ) + try: + return { + "image process": str( + f"{sp['number_of_processed_pictures']} image(s) processed " + f"({processes_applied_str}format: {sp['file_format']})" + ) + } + except KeyError: + return {} + + +class SleepyPiRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam SleepyPiRunner + """ + + configured_to_sleep: bool + start_sleep: str + stop_sleep: str + wait_for_ftp: bool + status: str + + +def get_SleepyPiRunner_report(sp: SleepyPiRunnerEntries) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + cs = sp.get("configured_to_sleep", False) + if not cs: + return {"sleep mode": "no"} + else: + with suppress(KeyError): + return { + "sleep mode": f"from {sp['start_sleep']} to {sp['stop_sleep']}\n{sp['status']}" + } + return {} + + +class SpaceKeeperRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam SpaceKeeperRunner + """ + + folder: str + disk: str + threshold: str + deleting: bool + free_space: float + + +def get_SpaceKeeperRunner_report( + sk: SpaceKeeperRunnerEntries, +) -> Dict[str, str]: + """ + Returns a summary of selected information + """ + disk = sk["disk"] + deleting = ( + f"*deleting older files (threshold={sk['threshold']})*" + if sk["deleting"] + else None + ) + return {"disk": "\n".join([s for s in (disk, deleting) if s])} + + +class StatusRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam StatusRunner + """ + + update: str + + +class ConfigRunnerEntries(RunnerStatusDict, total=False): + """ + For serializing the status of nightskycam ConfigUpdateRunner + """ + + updates: Dict[str, str] + + +def has_runner_status_dict(runner_class_name: str) -> bool: + """ + Returns True if the runner class has a corresponding implementation + of RunnerStatusDict, False otherwise. + """ + class_name = f"{runner_class_name}Entries" + try: + globals()[class_name] + return True + except KeyError: + return False + + +def has_status_entries_report_function(runner_class_name: str) -> bool: + """ + Returns True if the runner class has a corresponding implementation + of "get_report" function, False otherwise. + """ + fn: Callable[[RunnerStatusDict], Dict[str, str]] + fn_name = f"get_{runner_class_name}_report" + try: + fn = globals()[fn_name] + except KeyError: + return False + return isfunction(fn) + + +def get_runner_status_dict_class( + runner_class_name: str, +) -> Type[RunnerStatusDict]: + """ + Returns the corresponding subclass of RunnerStatusDict, assuming + it is defined in this module and named '{runner class name}Entries' + """ + try: + return globals()[f"{runner_class_name}Entries"] + except KeyError: + raise TypeError( + f"Not sublcass of RunnerStatusDict has been implemented for runner {runner_class_name}" + ) + + +def get_random_status_dict(runner_class_name: str) -> RunnerStatusDict: + """ + Arguments + The name of a nightskycam runner class. + + Returns + An instance of the subclass of RunnerStatusDict corresponding + to the class name passed as argument. + + Raises + TypeError if there is no instance of RunnerStatusDict corresponding + to the class name passed as argument. + """ + + def get_random_int(min_value: int = 0, max_value: int = 20) -> int: + return random.randint(min_value, max_value) + + def get_random_int_list() -> List[int]: + return [get_random_int() for _ in range(get_random_int())] + + def get_random_float( + min_value: float = 0.0, max_value: float = 1.0 + ) -> float: + return round(random.uniform(min_value, max_value), 2) + + def get_random_bool() -> bool: + return random.choice([True, False]) + + def get_random_str(length: int = 10) -> str: + return "".join( + random.choices(string.ascii_letters + string.digits, k=length) + ) + + def get_random_str_list() -> List[str]: + return [get_random_str() for _ in range(get_random_int())] + + def get_random_dict(): + return {get_random_str(): get_random_str() for _ in range(5)} + + random_fn = { + int: get_random_int, + float: get_random_float, + bool: get_random_bool, + str: get_random_str, + typing.Dict[str, str]: get_random_dict, + typing.List[int]: get_random_int_list, + typing.List[str]: get_random_str_list, + } + + runner_dict_class = get_runner_status_dict_class(runner_class_name) + fields = runner_dict_class.__annotations__ + kwargs = {} + for field, t in fields.items(): + kwargs[field] = random_fn[t]() # type: ignore + return runner_dict_class(**kwargs) + + +def get_status_entries_report( + all_status: Dict[str, RunnerStatusDict], # runner class name: entries +) -> Dict[str, str]: + """ + Returns a summary reports for all status for which + a corresponding report function has been implemented using the + name 'get_{runner_class_name}_report'. + + Arguments + all_status: key: the runner name, value: the corresponding + entries dictionary + """ + + def _get_function( + runner_class_name: str, + ) -> Callable[[RunnerStatusDict], Dict[str, str]]: + function_name = f"get_{runner_class_name}_report" + try: + return globals()[function_name] + except KeyError: + return lambda _: {} + + r: Dict[str, str] = {} + + for runner_name, entries in all_status.items(): + if entries: + report_fn = _get_function(runner_name) + report = report_fn(entries) + for k, v in report.items(): + r[k] = v + return r + + +class IntrospectionDict(TypedDict, total=False): + """ + TypedDict which keys correspond to the + Introspection django model + (package nightskycam-server) + """ + + cpu_temperature: int + camera_temperature: int + camera_target_temperature: int + outside_temperature: int + cooler_on: bool + upload_speed: float + free_space: float + + +def get_introspection_dict( + status: Dict[str, RunnerStatusDict] +) -> IntrospectionDict: + """ + Casting to IntrospectionDict + """ + intro_dict = IntrospectionDict() + for runner_class, status_dict in status.items(): + if runner_class == RunnerClasses.AsiCamRunner.value: + cr = cast(CamRunnerEntries, status_dict) + for key in ( + "camera_temperature", + "camera_target_temperature", + "cooler_on", + ): + with suppress(KeyError): + value = cr["camera_info"][key] + intro_dict[key] = value # type: ignore + + elif runner_class == RunnerClasses.LocationInfoRunner.value: + lir = cast(LocationInfoRunnerEntries, status_dict) + with suppress(KeyError): + cpu = lir["cpu_temperature"] + intro_dict["cpu_temperature"] = cpu + with suppress(KeyError): + outside = lir["temperature"] + intro_dict["outside_temperature"] = outside + + elif runner_class == RunnerClasses.FtpRunner.value: + fr = cast(FtpRunnerEntries, status_dict) + with suppress(KeyError): + up = fr["upload_speed"] + intro_dict["upload_speed"] = up + + elif runner_class == RunnerClasses.SpaceKeeperRunner.value: + skr = cast(SpaceKeeperRunnerEntries, status_dict) + with suppress(KeyError): + free = skr["free_space"] + intro_dict["free_space"] = free + + return intro_dict diff --git a/nightskycam_serialization/version.py b/nightskycam_serialization/version.py new file mode 100644 index 0000000..a4e2017 --- /dev/null +++ b/nightskycam_serialization/version.py @@ -0,0 +1 @@ +__version__ = "0.1" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..80f3c8c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,426 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, + {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, + {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, + {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, + {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, + {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, + {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, + {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, + {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, + {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, + {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, + {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, + {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, + {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, + {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "coverage2clover" +version = "4.0.0" +description = "A tool to convert python-coverage xml report to Atlassian Clover xml report format" +optional = false +python-versions = ">=3.6" +files = [ + {file = "coverage2clover-4.0.0-py3-none-any.whl", hash = "sha256:16bdb41f4765c8bf1dc3c9a609c3f2271233551dbcc298e64daf489ea170e030"}, + {file = "coverage2clover-4.0.0.tar.gz", hash = "sha256:17c42528b3c902f1819239b9b431ccc800e2c41e70e52acbe168477c7c767660"}, +] + +[package.dependencies] +coverage = ">=5.3,<8.0" +pygount = "1.2.4" + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, + {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, +] + +[[package]] +name = "mypy" +version = "1.11.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nightskyrunner" +version = "0.1.0" +description = "" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "nightskyrunner-0.1.0-py3-none-any.whl", hash = "sha256:9efb1f96fb64826bb768ee370c3841e436f89175644da8166db3368b709e906b"}, + {file = "nightskyrunner-0.1.0.tar.gz", hash = "sha256:93d46e735ee2ea399d7ad9f5674beccd8c97aca1408040f334fc968e5da21c5f"}, +] + +[package.dependencies] +jinja2 = ">=3.1.3,<4.0.0" +toml = ">=0.10.2,<0.11.0" +tomli = ">=2.0.1,<3.0.0" +tomli-w = ">=1.0.0,<2.0.0" + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pygount" +version = "1.2.4" +description = "count source lines of code (SLOC) using pygments" +optional = false +python-versions = ">=3.5" +files = [ + {file = "pygount-1.2.4-py3-none-any.whl", hash = "sha256:8ec56e58cfcb2be8bb54f32f02e7130d33302e2543c8a37b441e606ea3b8a2c5"}, +] + +[package.dependencies] +pygments = ">=2.0" + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.2" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +description = "A lil' TOML writer" +optional = false +python-versions = ">=3.9" +files = [ + {file = "tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7"}, + {file = "tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "5f8a599922e6385814feb4e3c920432140a1f513432225c87e2ee0b13c3c2f0e" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d6c59fa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[tool.poetry] +name = "nightskycam-serialization" +version = "0.1.0" +description = "communication between nightskycam and nightskycam-server" +authors = ["Vincent Berenz "] +packages = [{ include = "nightskycam_serialization" }] + + +[tool.poetry.dependencies] +python = "^3.9" +# Normal dependencies: +# Installable with both poetry and pip. +tomli = "^2.0.1" +tomli-w = "^1.0.0" +nightskyrunner = "^0.1.0" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +# Dependencies for development: +# Only installable with poetry (NOT pip). +isort = "^5.13.2" +mypy = "^1.10.0" + +[tool.poetry.group.test] +optional = true + +[tool.poetry.group.test.dependencies] +# Dependencies for testing: +# Only installable with poetry (NOT pip). +pytest = "^8.2.0" +coverage = "^7.6.1" +coverage2clover = "^4.0.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +# Coverage. +[tool.coverage.run] +branch = true +source = ["."] +omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +skip_empty = true + +# isort. +[tool.isort] +# Do not distinguish import style (import/from) for sorting. +force_sort_within_sections = true +# Use same formatting as black. +profile = "black" diff --git a/tests.yml b/tests.yml new file mode 100644 index 0000000..b136fbb --- /dev/null +++ b/tests.yml @@ -0,0 +1,33 @@ +name: unit tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-20.04 + + strategy: + matrix: + python-version: [3.9.2] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + poetry install --with test + - name: Run tests + run: | + poetry run pytest diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..3e81ca1 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,243 @@ +from pathlib import Path +import random +import string +import typing +from typing import Dict, List, get_args + +from nightskyrunner.status import RunnerStatusDict, StatusDict +import pytest + +from nightskycam_serialization.command import ( + CommandResult, + deserialize_command, + deserialize_command_result, + serialize_command, + serialize_command_result, +) +from nightskycam_serialization.config import ( + deserialize_config_update, + serialize_config_update, +) +from nightskycam_serialization.fix import deserialize_fix_dict, serialize_fix_dict +from nightskycam_serialization.serialize import ( + ImproperMessage, + IncorrectToken, + deserialize, + serialize, +) +from nightskycam_serialization.status import ( + NightskycamRunner, + RunnerClasses, + deserialize_status, + get_random_status_dict, + get_runner_status_dict_class, + get_status_entries_report, + has_runner_status_dict, + has_status_entries_report_function, + serialize_status, +) + + +def test_serialize() -> None: + """ + Checking serialize and deserialize functions. + """ + + din = {"A": 1, "B": "message", "C": {"c1": 9.2, "c2": ["c21", "c22"]}} + + message = serialize(din) + + dout = deserialize(message, required_keys=("A", "B", "C")) + + assert dout == din + + token = "supersecrettoken" + message = serialize(din, token=token) + + with pytest.raises(IncorrectToken): + deserialize(message, token="notthesametoken") + + dout = deserialize(message, required_keys=("A", "B", "C")) + + assert dout == din + + +def test_required_keys() -> None: + """ + Testing deserialize raises an ImproperMessage error + when a required key is missing. + """ + message = serialize({str(a): a for a in range(3)}) + + deserialize(message, required_keys=[str(a) for a in range(3)]) + deserialize(message, required_keys=("1",)) + deserialize(message, required_keys=[]) + + with pytest.raises(ImproperMessage): + deserialize(message, required_keys=("4",)) + + +def test_fix() -> None: + """ + Testing serialize_fix_dict and deserialize_fix_dict + """ + path = "/path/to/file" + + din = {"A": 1, "B": Path(path), "C": None} + + dfixed = serialize_fix_dict(din) + + assert dfixed["C"] is not None + assert not isinstance(dfixed["B"], Path) + + dout = deserialize_fix_dict(dfixed) + + assert dout["A"] == 1 + assert dout["B"] == path + assert dout["C"] is None + + +def test_command() -> None: + """ + Testing serialize_command and deserialize_command + """ + + command_id = 4 + command = "ls /" + + message = serialize_command(command_id, command) + + command_out = deserialize_command(message) + + assert command_out[0] == command_id + assert command_out[1] == command + + +def test_command_result() -> None: + """ + Testing serialize_command_result and deserialize_command_result + """ + command_id = 5 + command = "ls /" + stdout = "all ok" + stderr = "no error" + exit_code = "12" + error = "" + + result_in = CommandResult(command_id, command, stdout, stderr, exit_code, error) + message = serialize_command_result(result_in) + result_out = deserialize_command_result(message) + + assert result_in == result_out + + +def test_config() -> None: + """ + Testing serialize_config_update and + deserialize_config_update + """ + runner = "myrunner" + config = {"A": "a", "B": None, "C": 0.1} + + message = serialize_config_update(runner, config) + + out = deserialize_config_update(message) + + assert out[0] == runner + assert out[1]["A"] == "a" + assert out[1]["B"] is None + assert out[1]["C"] == 0.1 + + +def test_runner_classes() -> None: + """ + Testing consistancy between NightskycamRunner literals + and RunnerClasses enumeration. + """ + + for rc in RunnerClasses: + assert rc.name == rc.value + assert rc.name in get_args(NightskycamRunner) + for nr in get_args(NightskycamRunner): + assert nr in list(RunnerClasses.__members__.keys()) + + +def test_status() -> None: + """ + Testing serialize_status and deserialize_status. + """ + system = "my_system" + + class StatusTest1(RunnerStatusDict, total=False): + E11: str + E12: str + + class StatusTest2(RunnerStatusDict, total=False): + E2: str + + status1 = StatusDict( + name="status1", + entries=StatusTest1(E11="e11", E12="e12"), + activity="taking picture", + state="running", + running_for="3 minutes 4 seconds", + ) + + status2 = StatusDict( + name="status2", + entries=StatusTest2(E2="e2"), + activity="waiting", + state="sleeping", + running_for="12 minutes 5 seconds", + ) + + message = serialize_status(system, (status1, status2)) + + out = deserialize_status(message) + + assert out[0] == system + + all_status = out[1] + assert len(all_status) == 2 + + s1 = all_status["status1"] + assert s1 == status1 + + s2 = all_status["status2"] + assert s2 == status2 + + +def test_get_status_entries_report(): + runner_classes = tuple(NightskycamRunner.__args__) + + for runner_class_name in runner_classes: + assert has_runner_status_dict(runner_class_name) + + assert has_status_entries_report_function("CamRunner") + assert has_status_entries_report_function("CommandRunner") + assert has_status_entries_report_function("FtpRunner") + assert has_status_entries_report_function("LocationInfoRunner") + assert has_status_entries_report_function("SleepyPiRunner") + assert has_status_entries_report_function("SpaceKeeperRunner") + + assert not has_status_entries_report_function("StatusRunner") + assert not has_status_entries_report_function("ConfigRunner") + + for runner_class_name in runner_classes: + for _ in range(5): + runner_status_dict = get_random_status_dict(runner_class_name) + assert len(runner_status_dict) != 0 + report = get_status_entries_report({runner_class_name: runner_status_dict}) + if has_status_entries_report_function(runner_class_name): + assert len(report) != 0 + else: + assert len(report) == 0 + + def generate_random_instances() -> Dict[str, RunnerStatusDict]: + rc = random.sample(runner_classes, random.randint(0, len(runner_classes))) + return {r: get_random_status_dict(r) for r in rc} + + for _ in range(50): + runner_status_dicts = generate_random_instances() + + report = get_status_entries_report(runner_status_dicts)