From ff51b802e574dfde9917b24c37314d3ca2d24c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Doubravsk=C3=BD?= Date: Thu, 16 Apr 2026 08:49:32 +0200 Subject: [PATCH] Initial commit --- .python-version | 1 + .vscode/settings.json | 7 + AGENTS.md | 83 ++++ CLAUDE.md | 10 + DESIGN_DOCUMENT_MODULE.md | 260 ++++++++++++ PROJECT.md | 88 +++++ PlanetaryTime.code-workspace | 8 + README.md | 51 +++ poetry.lock | 370 ++++++++++++++++++ pyproject.toml | 26 ++ src/planetarytime/__init__.py | 13 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 531 bytes .../__pycache__/body.cpython-314.pyc | Bin 0 -> 3542 bytes .../__pycache__/epoch.cpython-314.pyc | Bin 0 -> 3154 bytes .../__pycache__/exceptions.cpython-314.pyc | Bin 0 -> 977 bytes .../planetary_time.cpython-314.pyc | Bin 0 -> 8192 bytes src/planetarytime/body.py | 55 +++ src/planetarytime/epoch.py | 51 +++ src/planetarytime/exceptions.py | 10 + src/planetarytime/planetary_time.py | 157 ++++++++ tests/__init__.py | 0 tests/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 152 bytes .../test_body.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 9871 bytes ...lanetary_time.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 21504 bytes tests/test_body.py | 33 ++ tests/test_planetary_time.py | 82 ++++ zkouska.py | 7 + 27 files changed, 1312 insertions(+) create mode 100644 .python-version create mode 100644 .vscode/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 DESIGN_DOCUMENT_MODULE.md create mode 100644 PROJECT.md create mode 100644 PlanetaryTime.code-workspace create mode 100644 README.md create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/planetarytime/__init__.py create mode 100644 src/planetarytime/__pycache__/__init__.cpython-314.pyc create mode 100644 src/planetarytime/__pycache__/body.cpython-314.pyc create mode 100644 src/planetarytime/__pycache__/epoch.cpython-314.pyc create mode 100644 src/planetarytime/__pycache__/exceptions.cpython-314.pyc create mode 100644 src/planetarytime/__pycache__/planetary_time.cpython-314.pyc create mode 100644 src/planetarytime/body.py create mode 100644 src/planetarytime/epoch.py create mode 100644 src/planetarytime/exceptions.py create mode 100644 src/planetarytime/planetary_time.py create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-314.pyc create mode 100644 tests/__pycache__/test_body.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_planetary_time.cpython-314-pytest-9.0.3.pyc create mode 100644 tests/test_body.py create mode 100644 tests/test_planetary_time.py create mode 100644 zkouska.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b4f594a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,83 @@ +# AI Agents - Project Rules + +**Document Version:** v4 (independent, incremented on structural changes) + +Rules and instructions for AI assistants (Claude Code, Cursor, Copilot, etc.) + +## First-time setup + +- **On first read of this file, immediately read all other `.md` files in the project root** (e.g. `PROJECT.md`, `CHANGELOG.md`, `DESIGN_DOCUMENT.md`) to get full project context before starting any task. + +## Language + +- **Always write all documentation in English** + +## Dependency Management + +- **Always use `poetry add`** to add dependencies, **never edit `pyproject.toml` directly** + ```bash + poetry add requests + poetry add --group dev pytest + ``` +- Use `poetry remove` to remove dependencies — **never edit `pyproject.toml` manually** + +## Project Structure + +- Entry points are in the project root (named after project or by purpose: `project_name.py`, `cli.py`, `gui.py`, `server.py`) +- A project can have multiple entry points +- All modules belong in the `src/` folder +- Tests belong in the `tests/` folder +- Virtual environment is in `.venv/` (do not copy, do not generate) + +## Code + +- Always use type annotations +- Follow PEP8 and format with Ruff (88 characters per line) +- Before commit run `poetry run ruff check` and `poetry run mypy` + +## Testing + +- **Use pytest exclusively** - never use the `unittest` module +- No `unittest.TestCase` classes, no `self.assert*` methods +- Use plain `assert` statements and pytest fixtures + +## Running + +- Use `poetry run` to run scripts: + ```bash + poetry run python project_name.py + poetry run pytest + ``` + +## Logging + +- Use **loguru** for logging - never use `print()` for debugging +- Never log secrets, passwords, tokens, or API keys + +## Environment and Secrets + +- Store secrets in `.env` file with `ENV_DEBUG=true/false` variable +- Load secrets using `python-dotenv` and `os.getenv()` +- **Never commit `.env` file** + +## Git + +- `.gitignore` should contain: `.venv/`, `__pycache__/`, `*.pyc`, `.mypy_cache/`, `.env` +- Do not commit `poetry.lock` only if it's a library (for applications, commit it) +- **Never commit this documentation** (`DESIGN_DOCUMENT.md`, `AGENTS.md`, `.claudeignore`) +- `PROJECT.md` **should be committed** - it's project-specific + +## Versioning + +- **Always ask user before bumping version** - never increase version automatically +- **Keep `CHANGELOG.md` updated** - document all significant changes as they are made +- Update `CHANGELOG.md` with changes before version bump +- Version is defined in `pyproject.toml` under `[project]` section +- Follow semantic versioning (MAJOR.MINOR.PATCH) + +## Task Management + +- **When completing tasks, mark them as done** - if you finish any task with a checkbox anywhere in project documentation, check it off as completed `[ ]` → `[x]` +- **Track all work** - this applies to tasks in `PROJECT.md` (TODO section, Development Roadmap, any checklists) and other documentation +- **Update documentation** - when completing changes, update relevant sections in `PROJECT.md`, `CHANGELOG.md`, and architecture diagrams +- **Keep task lists current** - completed items with `[x]` stay visible to show progress history diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c11f258 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,10 @@ + +## First-time setup + +**At the start of every new session, read all of the following files before doing anything else:** + +- `CLAUDE.md` (this file) +- `AGENTS.md` +- `PROJECT.md` +- `CHANGELOG.md` +- `DESIGN_DOCUMENT.md` \ No newline at end of file diff --git a/DESIGN_DOCUMENT_MODULE.md b/DESIGN_DOCUMENT_MODULE.md new file mode 100644 index 0000000..2ec4855 --- /dev/null +++ b/DESIGN_DOCUMENT_MODULE.md @@ -0,0 +1,260 @@ +# Python Library Development Guidelines + +**Document Version:** v1 + +> **Note on Versioning:** +> - This document version is independent — reused across projects +> - **Project version** source of truth: `pyproject.toml` under `[project]` +> - `CHANGELOG.md` uses project version from `pyproject.toml` + +## Related Documents + +- **README.md** — Project overview, public API description, installation and usage examples +- **AGENTS.md** — Rules for AI assistants +- **PROJECT.md** — Project goals and current state +- **CHANGELOG.md** — Version history + +### Documentation Organization + +All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. + +The root directory contains only the core documents: `DESIGN_DOCUMENT_MODULE.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`. + +--- + +## 1. Code Style + +- **PEP8** with 150-character lines (Ruff) +- **4 spaces** indentation +- **snake_case** functions/variables, **PascalCase** classes, **SCREAMING_SNAKE_CASE** constants +- **Type hints** required on all functions +- **Import order**: stdlib → third-party → local + +--- + +## 2. SOLID Principles + +- **SRP** — One class = one responsibility +- **OCP** — Open for extension, closed for modification +- **LSP** — Subclasses substitutable for parents +- **ISP** — Small interfaces over large ones +- **DIP** — Depend on abstractions + +--- + +## 3. Dependency Injection + +Pass dependencies via constructor. Never instantiate dependencies inside a class. + +--- + +## 4. Protocols Over Inheritance + +Prefer `typing.Protocol` and composition over class inheritance. + +--- + +## 5. Data Classes + +Use `@dataclass` for internal data structures, `pydantic.BaseModel` for data that requires validation. + +--- + +## 6. Logging + +This is a library. Libraries must **never configure logging sinks** — that is the responsibility of the consuming application. + +### Usage in library code + +```python +from loguru import logger + +def some_function() -> None: + logger.debug("Detail message") + logger.info("Milestone message") +``` + +Always use the module-level `logger` from loguru directly. Never call `logger.add()` or `logger.remove()` inside library code. + +### Behavior for consumers + +Loguru has a default sink to `stderr` enabled. Consumers who also use loguru get library logs automatically in their configured sinks. Consumers who want to suppress or filter library logs use the package name as the identifier: + +```python +from loguru import logger + +logger.disable("") # suppress all +logger.add(sys.stderr, filter={"": "WARNING"}) # WARNING and above only +logger.enable("") # re-enable +``` + +This must be documented in `README.md`. + +### Rules + +- Never call `logger.add()` inside library code — no file sinks, no stdout sinks +- Never call `logger.remove()` inside library code +- Never log secrets, passwords, tokens, or API keys +- Never use `print()` inside library code — not for debugging, not for output + +#### Log levels + +| Level | When to use | +|-------|-------------| +| `DEBUG` | Per-item detail: individual operations, cache hits, internal state | +| `INFO` | Significant milestones visible to the consuming application | +| `WARNING` | Recoverable issues: fallback used, unexpected but non-fatal state | +| `ERROR` | Failures the caller must know about — operation cannot continue | + +--- + +## 7. Environment and Secrets + +- Libraries do not use `.env` files or `python-dotenv` +- Configuration is passed by the caller via arguments or constructor parameters +- Never read `os.getenv()` inside library code unless explicitly documented as a supported configuration mechanism + +--- + +## 8. Error Handling + +Define specific exception types in `src//exceptions.py`. Use a fail-fast approach — surface errors early rather than silently continuing. All public exceptions must be exported from the top-level `__init__.py`. + +--- + +## 9. Testing + +- **pytest** only — no `unittest`, no `TestCase` classes, no `self.assert*` +- Arrange-Act-Assert pattern +- Test naming: `test__` +- Do not commit `poetry.lock` — this is a library; consumers pin their own dependencies + +--- + +## 10. Tooling + +| Tool | Purpose | +|------|---------| +| **Ruff** | Formatting and linting | +| **mypy** | Static type checking | +| **pytest** | Testing | + +Run before every commit: +```bash +poetry run ruff check +poetry run mypy +poetry run pytest +``` + +--- + +## 11. Poetry + +```bash +poetry install # Install all dependencies +poetry add # Add runtime dependency +poetry add --group dev # Add dev dependency +poetry remove # Remove dependency +poetry run # Run command in virtualenv +poetry build # Build sdist and wheel into dist/ +poetry publish # Publish to PyPI +``` + +Never edit `pyproject.toml` directly to add or remove dependencies. + +### pyproject.toml configuration for a library + +```toml +[project] +name = "your-library" +version = "0.1.0" +description = "Short description" +requires-python = ">=3.x" +dependencies = [] + +[tool.poetry] +packages = [{include = "your_library", from = "src"}] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" +``` + +Do **not** add `[tool.poetry.scripts]` — libraries have no entry points. + +### poetry.lock + +Do **not** commit `poetry.lock` for libraries. It is only committed for applications. Add `poetry.lock` to `.gitignore`. + +--- + +## 12. Project Structure + +``` +project/ +├── src/ +│ └── your_library/ +│ ├── __init__.py # Public API — export everything the caller needs +│ ├── exceptions.py # All public exception types +│ └── ... # Internal modules +├── tests/ # Tests +├── docs/ # Detailed documentation +├── .venv/ # Virtual environment (managed by Poetry) +└── pyproject.toml # Project config and dependencies +``` + +- No entry point scripts in the project root — this is a library, not an application +- `__init__.py` defines the public API; callers import from the top-level package only + +--- + +## 13. Public API + +- Everything intended for external use must be exported from `src//__init__.py` +- Use `__all__` to explicitly declare the public surface +- Internal modules are prefixed with `_` or kept unexported +- Public API must be stable across patch versions; breaking changes require a major version bump + +--- + +## 14. Distribution + +Build and publish with Poetry: + +```bash +poetry build # Creates dist/*.tar.gz and dist/*.whl +poetry publish # Publishes to PyPI (requires credentials) +``` + +`dist/` is **not committed** to the repository — add it to `.gitignore`. + +--- + +## 15. Versioning + +- Follow **semantic versioning**: `MAJOR.MINOR.PATCH` +- Version is defined in `pyproject.toml` under `[project]` +- Always ask before bumping the version — never increment automatically +- Update `CHANGELOG.md` before bumping the version +- Breaking changes to the public API require a major version bump + +--- + +## 16. Documentation and Task Management + +- Keep `PROJECT.md` and `CHANGELOG.md` up to date when making changes +- `README.md` must contain installation instructions and usage examples for the public API +- Document architectural changes in this file or in `docs/` + +### Task notation + +Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns: + +```python +# TODO: one-liner description of a task to be done +# FIXME: one-liner description of a known bug to be fixed +``` + +No other task format is used — no checkboxes, no numbered lists in documentation. + +If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`. diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..e3adecb --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,88 @@ +# PROJECT.md + +## Project Overview + +**PlanetaryTime** is a Python library for representing and working with time on other bodies in the Solar System, similar in spirit to the standard `datetime` module. + +--- + +## Core Concept + +Time on a given planetary body is expressed using a human-readable clock where **the hour is preserved as close to one Earth hour as possible**. + +The number of hours in a planetary day is derived from the length of that body's sidereal rotation period expressed in Earth hours, rounded to the nearest integer: + +``` +hours_per_day = round(rotation_period_in_earth_hours) +``` + +Example: if a body rotates once every 24.6 Earth hours, its day has 25 hours, each approximately 1 Earth hour long. + +--- + +## Epoch + +The starting point for counting days (sol 0 / day 0) is configurable per body and per use case: + +- **Discovery epoch** — the date the body was officially discovered +- **Contact epoch** — the date of first contact with the body: + - landing of an automated probe, or + - landing of a crewed mission + +The epoch is set when constructing a planetary time object and determines what "day 1" means. + +--- + +## Planned Public API + +```python +from planetarytime import PlanetaryTime, Body, Epoch + +# construct from Earth datetime + target body + epoch choice +pt = PlanetaryTime.from_earth(datetime.utcnow(), body=Body.MARS, epoch=Epoch.DISCOVERY) + +pt.day # integer day number since epoch +pt.hour # hour within the current day (0-indexed) +pt.minute # minute within the current hour +pt.second # second within the current minute +pt.body # the planetary body +pt.epoch # the epoch in use + +str(pt) # human-readable: e.g. "Sol 142, 09:35:22 (Mars / Discovery epoch)" +``` + +--- + +## Supported Bodies + +Initial target: planets and major bodies of the Solar System with known rotation periods. + +| Body | Rotation period (Earth h) | Hours per day | +|---------|--------------------------|---------------| +| Mercury | 1407.6 | 1408 | +| Venus | 5832.5 (retrograde) | 5833 | +| Mars | 24.6 | 25 | +| Jupiter | 9.9 | 10 | +| Saturn | 10.7 | 11 | +| Uranus | 17.2 | 17 | +| Neptune | 16.1 | 16 | + +Moon and dwarf planets (Pluto, Ceres, Eris) may be added later. + +--- + +## Current State + +- Project scaffolded (Poetry, src layout, tests directory) +- No implementation yet + +## TODO + +- Define `Body` enum with rotation periods +- Define `Epoch` enum and epoch registry +- Implement core `PlanetaryTime` class +- Implement conversion from Earth `datetime` +- Implement `__str__` / `__repr__` +- Write tests for conversion accuracy +- Write tests for epoch switching +- Populate README with usage examples diff --git a/PlanetaryTime.code-workspace b/PlanetaryTime.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/PlanetaryTime.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1736527 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# PlanetaryTime + + + +## Installation + +```bash +pip install planetarytime +``` + +## Usage + +```python +import planetarytime + +# TODO: usage examples +``` + +## Logging + +This library uses [loguru](https://github.com/Delgan/loguru) for internal logging. + +By default, loguru has a sink to `stderr` enabled. If your application also uses +loguru, library logs will appear in your configured sinks automatically — no extra +setup needed. + +### Suppressing library logs + +To silence all logs from this library: + +```python +from loguru import logger +logger.disable("") +``` + +To re-enable them: + +```python +logger.enable("") +``` + +### Filtering by level + +If you want library logs only from `WARNING` and above, filter by the package name: + +```python +from loguru import logger +import sys + +logger.add(sys.stderr, filter={"": "WARNING"}) +``` diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0867df6 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,370 @@ +# This file is automatically @generated by Poetry 2.3.2 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" +groups = ["main", "dev"] +markers = "sys_platform == \"win32\"" +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 = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "librt" +version = "0.9.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443"}, + {file = "librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c"}, + {file = "librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285"}, + {file = "librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2"}, + {file = "librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38"}, + {file = "librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b"}, + {file = "librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774"}, + {file = "librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8"}, + {file = "librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671"}, + {file = "librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d"}, + {file = "librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1"}, + {file = "librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882"}, + {file = "librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076"}, + {file = "librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a"}, + {file = "librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6"}, + {file = "librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8"}, + {file = "librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a"}, + {file = "librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4"}, + {file = "librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d"}, + {file = "librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27"}, + {file = "librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2"}, + {file = "librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8"}, + {file = "librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f"}, + {file = "librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f"}, + {file = "librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745"}, + {file = "librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9"}, + {file = "librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e"}, + {file = "librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22"}, + {file = "librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5"}, + {file = "librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11"}, + {file = "librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2"}, + {file = "librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d"}, + {file = "librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd"}, + {file = "librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519"}, + {file = "librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5"}, + {file = "librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb"}, + {file = "librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499"}, + {file = "librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1"}, + {file = "librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f"}, + {file = "librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b"}, + {file = "librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b"}, + {file = "librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9"}, + {file = "librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e"}, + {file = "librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4"}, + {file = "librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938"}, + {file = "librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c"}, + {file = "librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15"}, + {file = "librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40"}, + {file = "librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118"}, + {file = "librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61"}, + {file = "librt-0.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4"}, + {file = "librt-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a"}, + {file = "librt-0.9.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f"}, + {file = "librt-0.9.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7"}, + {file = "librt-0.9.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc"}, + {file = "librt-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6"}, + {file = "librt-0.9.0-cp39-cp39-win32.whl", hash = "sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15"}, + {file = "librt-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1"}, + {file = "librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d"}, +] + +[[package]] +name = "loguru" +version = "0.7.3" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = "<4.0,>=3.5" +groups = ["main"] +files = [ + {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, + {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==0.910) ; python_version < \"3.6\"", "mypy (==0.971) ; python_version == \"3.6\"", "mypy (==1.13.0) ; python_version >= \"3.8\"", "mypy (==1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] + +[[package]] +name = "mypy" +version = "1.20.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "mypy-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3ba5d1e712ada9c3b6223dcbc5a31dac334ed62991e5caa17bcf5a4ddc349af0"}, + {file = "mypy-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e731284c117b0987fb1e6c5013a56f33e7faa1fce594066ab83876183ce1c66"}, + {file = "mypy-1.20.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e945b872a05f4fbefabe2249c0b07b6b194e5e11a86ebee9edf855de09806c"}, + {file = "mypy-1.20.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fc88acef0dc9b15246502b418980478c1bfc9702057a0e1e7598d01a7af8937"}, + {file = "mypy-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:14911a115c73608f155f648b978c5055d16ff974e6b1b5512d7fedf4fa8b15c6"}, + {file = "mypy-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:76d9b4c992cca3331d9793ef197ae360ea44953cf35beb2526e95b9e074f2866"}, + {file = "mypy-1.20.1-cp310-cp310-win_arm64.whl", hash = "sha256:b408722f80be44845da555671a5ef3a0c63f51ca5752b0c20e992dc9c0fbd3cd"}, + {file = "mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e"}, + {file = "mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca"}, + {file = "mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955"}, + {file = "mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8"}, + {file = "mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65"}, + {file = "mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2"}, + {file = "mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10"}, + {file = "mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51"}, + {file = "mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28"}, + {file = "mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f"}, + {file = "mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37"}, + {file = "mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237"}, + {file = "mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d"}, + {file = "mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019"}, + {file = "mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1"}, + {file = "mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184"}, + {file = "mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b"}, + {file = "mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e"}, + {file = "mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218"}, + {file = "mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2"}, + {file = "mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895"}, + {file = "mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12"}, + {file = "mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe"}, + {file = "mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08"}, + {file = "mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572"}, + {file = "mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6"}, + {file = "mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3"}, + {file = "mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4"}, + {file = "mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a"}, + {file = "mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986"}, + {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a"}, + {file = "mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9"}, + {file = "mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02"}, + {file = "mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa"}, + {file = "mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08"}, + {file = "mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06"}, + {file = "mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804"}, +] + +[package.dependencies] +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.15.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f"}, + {file = "ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e"}, + {file = "ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609"}, + {file = "ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f"}, + {file = "ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151"}, + {file = "ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8"}, + {file = "ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07"}, + {file = "ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48"}, + {file = "ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5"}, + {file = "ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed"}, + {file = "ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188"}, + {file = "ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, + {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, +] + +[package.extras] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.14,<4.0" +content-hash = "23e204d0d68650008db039ff23151ebaa1d69730287ca59d2c0eea7c9edbd67d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..30a84ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "planetarytime" +version = "0.1.0" +description = "" +authors = [ + {name = "Jan Doubravský",email = "jan.doubravsky@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.14,<4.0" +dependencies = [ + "loguru (>=0.7.3,<0.8.0)" +] + +[tool.poetry] +packages = [{include = "planetarytime", from = "src"}] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[dependency-groups] +dev = [ + "pytest (>=9.0.3,<10.0.0)", + "mypy (>=1.20.1,<2.0.0)", + "ruff (>=0.15.10,<0.16.0)" +] diff --git a/src/planetarytime/__init__.py b/src/planetarytime/__init__.py new file mode 100644 index 0000000..adccedb --- /dev/null +++ b/src/planetarytime/__init__.py @@ -0,0 +1,13 @@ +from planetarytime.body import Body +from planetarytime.epoch import EpochType +from planetarytime.exceptions import DatetimePrecedesEpochError, EpochUnavailableError, PlanetaryTimeError +from planetarytime.planetary_time import PlanetaryTime + +__all__ = [ + "Body", + "EpochType", + "PlanetaryTime", + "PlanetaryTimeError", + "EpochUnavailableError", + "DatetimePrecedesEpochError", +] diff --git a/src/planetarytime/__pycache__/__init__.cpython-314.pyc b/src/planetarytime/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd78f59b9e12519af386ebe8ace8100aec93e402 GIT binary patch literal 531 zcmZ9J%Sr<=6oyac+6yXH1W}=jt_GpAb0vb(%FISfsawNz8t9a{kx9jIqqy`<-1shT z14Y4=JFzcdlG;Me;?JLRa=vpyjy5-HAlB={NA!#UKILJ#v_CVy5atnXppSg;5JOGr zHDB{|rYo=ehG#O z%A7bl-|qyBuqYutNI3o%IBf<0=r}`mwou* z`o+}=%T{V0Zetw8F~%=27Zt$I+;Nr=A?bbKcCv7qkdzf}Z#krVtXt5~UBM*F#W+eM xhH*PDdF3R@M$?#_(rrN$p9{r-kZ&C!G=tU*oOhVK!DI%0sC6DTi3Oo-DwsCbkXLL9(AcsOC>M5;(wZS0NXV(+H^{yv05 zY^J|; z&U`cb&%PS)HxXEW&EL!2auM5>AUu+(E)* zCy~T9A~`0-{SNCM=9qMhc*~Be|B@4M{=_PVn&bl9SC4Vr4R|?@8nfg9JX8bNs^kT{ zr2)PU@C^;{rali_KMDC<7IqNW(-hI(ruY%&8o=UIt7C8BHh0~DXQ_}?1fgy)P3E<0!6 z+E%zIkqHi>7$@DYR4xdVL%7m8n#-tERq_#v&(G;vWLBj)J%c4lD3r}aX7nO8dZ8JE zvNW6;Obrbu<QQ`0X)}nJNLx`y%Ip&H=;%g7+dyEfwTwoQ%IJ+E|EtCUyZHJqjP?xE zsT>1BZ5|b>7qyJl-bJketpRyhIyRIVJZG^=x4w#Hycy-% zR8GFg98@9FX*Y1WN`23tKivSpd!uHcP!8G&52L$KG_DmsiI)nU+qJ60w!^(f1Vl}( z_AG_>d=|ai)xXr$zZ~wr6Fj=;J<932O_DIGyt1q*1w4|QWVxVcih0!Guzshg?b8u0P6v*%u*t#oR|*IG)ftZy!zSmBrcaOva<7kGKk z<$o%|)LC;7cVHE@RWE3WMJrgef^}7}t_n85u_thc)s#Gz>@==FKa6J~9#Z=5>v;C# z8@LX|vp;-~t37V*ncf}G?&IqigisusROkfL$3lZMTvM zd^P!+Kp&RoE^GV5+Oc_9l!Oj2T@wBWPa=APGtEKq_@;P<(w!h;PIC*CRM#>&oXJYQ z{DA-$SZ^nOe{lXNerZCnap3W(v8!g(;k?5JFwTG|k-H?kM8Z!`3SL2cNI+cQa6NM~ zv2c00dDjEHesD@4!p5f))eX+;shbBDE-r_5@(m9saRYxxlG$$`cTRlp?yY#1mnrmB zJp0Y|tG_R8UW{j-faTYzKjPV+KAr7<`)Y8IS9jBu4oe9&9UP1WXdI$4o$zf}pjb95 zp982JMDaR`LnyFP^i33pQB-K+109l&1Im-PI^Fahv0m4s^f*{J17CvjY}FwM!Y?HJ zBMCopZxaUI-~Wif-|8`exYk`eH-B!SW5wycc4q#Jr3}pvEkqGPQ#&eW=g(S5ML|<^ uHR$aSOTjfHadlJKS$EkPSI`+(@D8q^kDjtuuAr$0ob47%fyd~KQ~fupPwA5Y literal 0 HcmV?d00001 diff --git a/src/planetarytime/__pycache__/epoch.cpython-314.pyc b/src/planetarytime/__pycache__/epoch.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b26d21598d6fcf60f37bc399702ba9a0a961385 GIT binary patch literal 3154 zcmbtV-A@!(6u&b&><+N*;>rqsw2Yv@s%0B%{H`tHMizC&VcFIMW;*T;GU+mdckZGP zQ`g4S$J&GkA84|Vnlw@SV$<}GzzC5}H8uTs@D0U=KK9%@yX>N%B)!R;`*A+*Ilptx zon3)-H3&xG>NDejh|r&OQyTTf#ljaJgfx^#a;O23@P~*X1<72T>*fg8%@e+f?)kW< zTOa~kd*W5yUgBkIA@1uIiP(g~Xd9BN8j$QA6!wBeI!N`Px3_eUd&KA<2eLk;R`Ct; zy&EeS#icCn7u+T2H@?Yd`qc`jaBI-t{6F=#zD0j6{=dJfzE9b-2LGj1^lKE4vThCf z&8z6wD%DEEyY!n*ulr}&lN8uVAoWkB&M^=r^yA(wIx z{8}ML?0g06+4%~X%0a+t17O8btmHcAg!K(5TSw~UU<-DvrI5ujfhx<)!Of$0D`K%tp*uv?`RC2}50(B;j1`*+8M0iCc z9+@M8%#$kFL%gy;d~y|d;B~5*(d783KGD)`U6RPKF4;uG#4s;N8EsrLY$>A?9gi5M zp6PI^GlreE#&kUH_|le1v^2Sc5)si6Rn^o+bX9d~RCUD4pGK2c6KG5u&RP5#7L_eA=o&dBf71s*bkvC z&^Vo=(P3*u2VzcW(c@M+H=>)wj`n6XQ-=`7lYnK^#_8y2>4c&i)hUb}qvN<5j4%Sv zAhLr%zCphVA3Uqul8?^?8m^zf;gW=*dLdJs<0TV@Ib<0anWSDuK*7UaXTGHX|NPFLUFfS5k+EEIgtSn|jlX>AOmEqM4=z&_aeVE%P?` z>}^2uXc76j4KFB}s-LIR4+q)wOCvjEh@_WX1eGfI;4Wcdis`2+g6XZ411=psC=;N8 zmqSaT-4!j|f(B>|Y%J%D@+p1@Bte%Zjt!Yhp0Zfu6T`wr(G)kzk~>Y|d@JLy|Eh z6&s-_4;_Q`i#gpUdPah(l}u|%Vpy{!E1k|^49!$CQI{yQE*WH4%0(dnsZvV%BR{#M z?m7e)haEGtj`Ik#fEbC7zz|QRbw;)C>RPo-a<(&5Svy_ zYpdiIO5lP{RJY71PPhe_tW-|IB9G?%NDNJ$dmL_m7;b;$Ykv}KDhRW|9n<2DzXDrt zC1(T8`4e-#z}Huw1X`wB_uV58Chkw%%T6D=IK3S|3fR-4Jy#pbC)}c<$totSR8(l; zXLi$@JrjlhPq}Ljf>Z{AfYMkhxpIM(3+f}jveXXqGS>>4N=3;sOU>`LB=fr|j>YkY zT@STOI~H$(1(tv$Fm14i;A~MBwhk&So<*v8ZWOkW^J(BV&nX{S4(Wp8N*Z58IO)3}_WUry_zG(?*f z9IH^rcRD6_^}`$Q$&s{Em5L?$`yAnPr`+fGdis0g$(ZbT`#O{Ta>DWU%bk?%OT>DU z{fQU@kzvT{kU;?OP)`{=pSm?G9&!IYNAVuuFpNF}`tQ7lt;f!Lx5>voWP0ox_{=zW3hl`@VTkZtZLlJm0?jlqn_TmkX-XSUU4J z=$w-_*(Wi%M`GG1&w?^%sYh#w27S6hYaXp5+E_z_c;g{?yqQ$rcQ^jipZOkM=cG?8 zjft&&pf-r9t;aRnhy(1Z&+Z&&A_pt<^qm|*Piv*mp1u|a7<^7(Y^BQCiPB7D85>un z7E2kpTGP28(Z$BB$$2hD!1-*8^N~u6%=z1#pB5rpW_CG0k=ocy=8!8)Z*rch1f4eL z#tJJFF09pZP*^aW>pBiUcRXf?;ZThLO@1cAu1bm#Z%axJk8opKy=pPb7xg;$PPZ@r1e!HpmW%jG-^GM~^bnoj0i|?Nqx8l1`wN2C aqDkoyy?oGd(GhiFL@)Ob{$LnU|G~do{p`5_ literal 0 HcmV?d00001 diff --git a/src/planetarytime/__pycache__/planetary_time.cpython-314.pyc b/src/planetarytime/__pycache__/planetary_time.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58b19d7aae402999d1f1935691820ba796c78636 GIT binary patch literal 8192 zcmcIpO>7)R7Ve%u&y2_8@lWEPn7D1n#1kAl#OBZB2S@^TfM7ai48qCKnQ7Yt|&`Jp>tUZ|nN+Ka`_5_@kBggTY$W{xqkv3?BvA}+ZMF!)Kd9m)k6jQqUq|7*a9WVX31M6`#cnFQ zCS2n#(>3lk-MsFe@Qiy+FE4v0eB*Uy9WQ$){Nn*LFdj66yzZN*9}k(KZeg3yBgl1a zg6to4*>5r%nInbI{Q>azSv#Z=AIf@=9G&*_>9CSO{gn^PG| zo~QKWBKQKwGJ0ZuYB8f);W^cet5l6sXE1dhEetZr)P$N;4bFUwF`XrBDeSyBdA~@L zKP8x=ESRp-A=533rbl)`aCe)Kj+ow4zEgFRk@C)Vc|?>w;PMhzS{yAfn1NG4*#|nN zo{ki$^`QDe4OCD=pawy$ub?)78UnSUf*J-j3~FNqwGq?^s7)2r2&m1Vwp37?d^CrG&1TO2Ui|NmKKxlt}A|H_$(5B$PCI8@go9D<w8hm_1#>ma zM#BK;X7^u2D-EEtGbvL^JN<{NM;vs9gibKAtd=sRUX0R9f@evSv(SN(PD`4u?X@L` z4pXWDr5mQIK-IHSZ&F$89SSC}Is*ME`SEw7x;cQtc_Aj8c0Kx@AS6NS5?&PNMOl%{7)7_|43iT%ZBOsm6$Qqpll#hN6IxRKHls=etf z?`&FEOv}eqGs`r~a{}tVMqAs5Su7}~LBjP~&kWD&3o0@#rwkv}6WIl_rr`=x8#Y*C zxM(+IU8P!_szaGYD-@3xFhh;Ut4@gE2-A}0=wF!Lrxkz> zdq&83V&bH)$XG1IJcHh1MaX!;MOv;(F>eKzZ>HX1kj0qW0n|+0OoKz8K08=^yC94f zq>2d|EhiK?~YYGPDC3mZ=CkNRbTLY41El@%No(d$k(zaVv0DR zLlDhJYb>YGTPj%i+Z!rn4={B>Ndd)~K_+=Wl0=CSMp*uMJOvB}nryGoEzrcKb0oP9 z*7L$fNZ8)_$Kd*o{;&P*>pLI$+P~vw??4{qp8mC-k(Hj2{KHS*=z01=<9hG?YrUf@ zy`$HEd!zShaNKI&v2-NgGq&3P_=SdBo%by*+}L?|weyJ!9*o!8bE9?iLf}?M*U~FD z?t5&tV+`cLy1(g$e;3H$PTkn~_-f|?&Q0Wgkh{8<16Sow8dtmafgbvX*Pr?%yxKKP z^;`b%rO*eV_0XyHP~_6_i^tbP+t)%}E1|ApJ#uN{;zU{Pxr@(H4UZq20)Zf-9rXWj zF^o2m<%eaMh|^3)*lMpdVUxYwWc!RDhJ`5GhAb*F+9kFR;j6`Hce4&;N2tNU_df6F zZ2{TM=FSZ-p1}9UmW?_p`3dO*R06)Y-L6N;o>n=&6M6vO8#Ya#COoG&2|{(6?IAyk zHgHy_nc`@*5MLG_DWGvioGGDVUlGMU)2)L=yc5VLcqfa9R(h8cOJ;mCq$8ec;S-_U zbaPFNa@V4c7NqYMV!qSDqd>9bUYhGn5%FbrObiLKCswmRTD-u|8WA#Dd@&F5y8QtM zt>?uU4hGYGG2duGlfXK^@�MgsBMLB}J;(iP9|77bsY?gCXaTfS~e8HfM|lX)imb zHCDwW4N&AjCyBkvTL|AMzSNY|Syg&z>PU2m_a*cjlcp4gvqV`iNo1R$6c3F_)AOms zd?9Q_bcujXLP*JjzkP#*w9-BZCi8HpAY0{RD2PK4tnx6=L z=q}6;0Vz}8%p&GRRC*?BA|iFda%mp3))R>=Lo7cOq=3)NrLK+Y8%iU!$3*aB7^Q!{BLp)!i9 zEO*vSFxq2mh%)YJJR}&4Qk`~Lw9axT(gqtLLebvHSk zBx{Uwd$ZarmUgJr@)ULb3>kdJ9wF3v>F~wFf2;3ZZ|h!b+r84Z`_prOe&^ad`L^Aw zZ3ixl-)iYu-nH5?a>2hIX}=NayWm}Kzi+MmftB_LR@?W&T>#1JKd8^Qj(;_U93K*VkUlth|=_+W+Rw`u3#+~xFg@AAvb$FH8*aEnd*H{I@*@P^=S2;UA0k(SG&%coWwNAmuWJCUD4 zOKewhlVYc>4j02Bwz8X}y$$6Y-tPwG3#2T98Vc1#Z|7~wE+_e;BnaAhA|!<%flYEw zBuFi+?h~$wC*VFv*lQx&g9rB__uF2C7Ts#DTs+!qY2qVjJulpBY`K)Xn9KV+N+J|M zT=?H~!5!S)bnzxhP|}4&N|2T&_@g8Un0UHJU9cy?4QTdg+B!z0Zq8zPzs?oC@` z2ty*Rzub!0I0P?k<4x~jAyIft17Z(vOmn8n#26k`i`Yp*gL0K`#MTMt(x1fP>x^SwY z6^a9@l+ur}@3bO*q^YtZqWy-rpQo%S-mj#H_wbUk-PJ_~)3xO0kJ8J*#M8a&=7Hz= zp|6&9XoA)1xgW#dp&no1;MiJeHa7M5=uxrJ-nsZr-oK+H^RJ|^izMkZWyPgIg{OPh z&fb&!GOd<&?AcZ8XE$DOr+)0SuvMiWn|gtIyh>S7&Y!K3a$YSv-n4Ex7!1+6dUH5^t;9-$;j^TuX&v4^RwpFy|k% z{m{W&-JpGklk@ST1t(DKe9&^^D#sGXEHN7O^C-s-Z@Gu({)%HT#~i-1YMKwOJ`g*z zT2~)7oiFA&OaR_Lj2QLGo{#se_3mH6ujirbFMpm~J9J{@(22D}Q!Dr#oJOqqve>%b z+Wyh;5076yuyl4gc_sC6YWY<(Jm}DujC@Pq@`KA$S7tt*x!#xG_3(A&bI)HxUxdCq znSW&BtELUFtKm6u!|kbc198u>7gHejUCng~Om~`V%iYZNUtA|!nQOr?YZxtB$OLPc ztn7w4k*U=%&th8*1LbYr+9q1dG$8>QZ4JZRg4;1wFOQbU5-A|ax$qq$Ng7rudl*^H zEe%V#I@@;1EVr~niptCE^(j=#qH|mm#(^}j%54?90~d0f%$JN}LNdyh%SWqi?Ye8- zj@_5~^5LCJ%ChH5=;P4UllkrkuQy#E{p`SB4t(j(?>`EgJSIlgBds3=KMbx#q?L%2 zZ|qraS)N+!e`uxup|$?USNb2%?>g}LGh~s+$}G~^b9}p1ZL(c7xt)~J{7*!;(G8=f zEYSCgU_8E{C$niP<4eYyStVU)X^hADXRmZhQ#BpUP(o5qV1w?%C{}$Utr*6FYR>CP z`nief z75U0&KSbbqIn6>oc3hhb*iQgIf~pi%Ks_{3~>pW$g=v=8W@slYf^Y z*tS2NwNy6na}-}MekEbGS2dqYs2Tq8o8L>6E float: + """Sidereal rotation period in Earth hours.""" + return _ROTATION_HOURS[self] + + @property + def orbital_hours(self) -> float: + """Orbital period around the Sun in Earth hours.""" + return _ORBITAL_HOURS[self] + + @property + def hours_per_sol(self) -> int: + """Number of hours in one sol (rotation period rounded to nearest hour).""" + return round(self.rotation_hours) + + @property + def sols_per_year(self) -> int: + """Number of sols in one planetary year (orbital period / rotation period, rounded).""" + return round(self.orbital_hours / self.rotation_hours) + + +_ROTATION_HOURS: dict[Body, float] = { + Body.MERCURY: 1407.6, + Body.VENUS: 5832.5, + Body.MARS: 24.6, + Body.JUPITER: 9.9, + Body.SATURN: 10.7, + Body.URANUS: 17.2, + Body.NEPTUNE: 16.1, +} + +# Orbital periods in Earth hours +_ORBITAL_HOURS: dict[Body, float] = { + Body.MERCURY: 87.97 * 24, + Body.VENUS: 224.70 * 24, + Body.MARS: 686.97 * 24, + Body.JUPITER: 4332.59 * 24, + Body.SATURN: 10759.22 * 24, + Body.URANUS: 30688.50 * 24, + Body.NEPTUNE: 60182.00 * 24, +} diff --git a/src/planetarytime/epoch.py b/src/planetarytime/epoch.py new file mode 100644 index 0000000..9fdb985 --- /dev/null +++ b/src/planetarytime/epoch.py @@ -0,0 +1,51 @@ +from datetime import datetime, timezone +from enum import Enum + +from planetarytime.body import Body +from planetarytime.exceptions import EpochUnavailableError + + +class EpochType(Enum): + """How the starting day is determined.""" + + DISCOVERY = "discovery" + CONTACT = "contact" + + +# Discovery dates for Solar System bodies (UTC midnight). +_DISCOVERY_DATES: dict[Body, datetime] = { + Body.MERCURY: datetime(1631, 11, 7, tzinfo=timezone.utc), # first recorded transit (Gassendi) + Body.VENUS: datetime(1610, 1, 1, tzinfo=timezone.utc), # telescopic observation (Galileo) + Body.MARS: datetime(1610, 1, 1, tzinfo=timezone.utc), # telescopic observation (Galileo) + Body.JUPITER: datetime(1610, 1, 7, tzinfo=timezone.utc), # moons discovered (Galileo) + Body.SATURN: datetime(1610, 7, 25, tzinfo=timezone.utc), # rings observed (Galileo) + Body.URANUS: datetime(1781, 3, 13, tzinfo=timezone.utc), # Herschel + Body.NEPTUNE: datetime(1846, 9, 23, tzinfo=timezone.utc), # Le Verrier / Galle +} + +# First contact dates — automated probe landing or crewed landing. +# None means no contact has occurred yet. +_CONTACT_DATES: dict[Body, datetime | None] = { + Body.MERCURY: datetime(2011, 3, 18, tzinfo=timezone.utc), # MESSENGER orbit insertion (closest approach) + Body.VENUS: datetime(1970, 12, 15, tzinfo=timezone.utc), # Venera 7 — first soft landing + Body.MARS: datetime(1976, 7, 20, tzinfo=timezone.utc), # Viking 1 — first soft landing + Body.JUPITER: None, + Body.SATURN: None, + Body.URANUS: None, + Body.NEPTUNE: None, +} + + +def get_epoch_date(body: Body, epoch_type: EpochType) -> datetime: + """Return the epoch datetime for a given body and epoch type. + + Raises: + ValueError: if contact epoch is requested but no contact has occurred. + """ + if epoch_type is EpochType.DISCOVERY: + return _DISCOVERY_DATES[body] + + contact = _CONTACT_DATES[body] + if contact is None: + raise EpochUnavailableError(f"No contact with {body.value} has occurred — contact epoch is unavailable.") + return contact diff --git a/src/planetarytime/exceptions.py b/src/planetarytime/exceptions.py new file mode 100644 index 0000000..2411509 --- /dev/null +++ b/src/planetarytime/exceptions.py @@ -0,0 +1,10 @@ +class PlanetaryTimeError(Exception): + """Base exception for all planetarytime errors.""" + + +class EpochUnavailableError(PlanetaryTimeError): + """Raised when a requested epoch has not occurred yet for a given body.""" + + +class DatetimePrecedesEpochError(PlanetaryTimeError): + """Raised when the provided datetime is earlier than the body's epoch.""" diff --git a/src/planetarytime/planetary_time.py b/src/planetarytime/planetary_time.py new file mode 100644 index 0000000..9421344 --- /dev/null +++ b/src/planetarytime/planetary_time.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from loguru import logger + +from planetarytime.body import Body +from planetarytime.epoch import EpochType, get_epoch_date +from planetarytime.exceptions import DatetimePrecedesEpochError + + +class PlanetaryTime: + """Represents a point in time expressed on a specific planetary body. + + Time is anchored to an epoch (discovery or first contact). The clock is + scaled so that one planetary hour is as close to one Earth hour as possible: + - hours_per_sol = rotation period rounded to nearest Earth hour + - sols_per_year = orbital period / rotation period, rounded + + The unit "sol" is used for all non-Earth bodies instead of "day". + """ + + def __init__( + self, + body: Body, + epoch_type: EpochType, + total_seconds_since_epoch: float, + ) -> None: + self._body = body + self._epoch_type = epoch_type + self._total_seconds = total_seconds_since_epoch + + seconds_per_hour: float = 3600.0 + seconds_per_sol: float = body.hours_per_sol * seconds_per_hour + seconds_per_year: float = body.sols_per_year * seconds_per_sol + + total_seconds = max(0.0, total_seconds_since_epoch) + + self._year: int = int(total_seconds // seconds_per_year) + remainder = total_seconds % seconds_per_year + + self._sol: int = int(remainder // seconds_per_sol) + remainder %= seconds_per_sol + + self._hour: int = int(remainder // seconds_per_hour) + remainder %= seconds_per_hour + + self._minute: int = int(remainder // 60) + self._second: int = int(remainder % 60) + + logger.debug( + "PlanetaryTime constructed: body={} epoch={} year={} sol={} {:02d}:{:02d}:{:02d}", + body.value, + epoch_type.value, + self._year, + self._sol, + self._hour, + self._minute, + self._second, + ) + + # ------------------------------------------------------------------ + # Factory + # ------------------------------------------------------------------ + + @classmethod + def from_earth( + cls, + earth_dt: datetime, + body: Body, + epoch_type: EpochType = EpochType.DISCOVERY, + ) -> PlanetaryTime: + """Create a PlanetaryTime from an Earth datetime. + + Args: + earth_dt: Earth datetime (timezone-aware or naive UTC). + body: Target planetary body. + epoch_type: Which epoch to use as year 0, sol 0. + + Returns: + PlanetaryTime instance for the given moment on the target body. + + Raises: + DatetimePrecedesEpochError: if the datetime precedes the epoch. + EpochUnavailableError: if contact epoch is requested but no contact has occurred. + """ + if earth_dt.tzinfo is None: + earth_dt = earth_dt.replace(tzinfo=timezone.utc) + + epoch_dt = get_epoch_date(body, epoch_type) + delta = earth_dt - epoch_dt + total_seconds = delta.total_seconds() + + logger.info("Converting Earth datetime {} to {} time (epoch={})", earth_dt, body.value, epoch_type.value) + + if total_seconds < 0: + raise DatetimePrecedesEpochError( + f"Earth datetime {earth_dt} precedes the {epoch_type.value} epoch " + f"for {body.value} ({epoch_dt})." + ) + + return cls(body, epoch_type, total_seconds) + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ + + @property + def body(self) -> Body: + return self._body + + @property + def epoch_type(self) -> EpochType: + return self._epoch_type + + @property + def year(self) -> int: + """Year number since epoch (0-indexed).""" + return self._year + + @property + def sol(self) -> int: + """Sol number within the current year (0-indexed).""" + return self._sol + + @property + def hour(self) -> int: + """Hour within the current sol (0-indexed).""" + return self._hour + + @property + def minute(self) -> int: + """Minute within the current hour (0-indexed).""" + return self._minute + + @property + def second(self) -> int: + """Second within the current minute (0-indexed).""" + return self._second + + # ------------------------------------------------------------------ + # String representation + # ------------------------------------------------------------------ + + def __repr__(self) -> str: + return ( + f"PlanetaryTime(body={self._body.value!r}, epoch={self._epoch_type.value!r}, " + f"year={self._year}, sol={self._sol}, " + f"time={self._hour:02d}:{self._minute:02d}:{self._second:02d})" + ) + + def __str__(self) -> str: + return ( + f"Year {self._year}, Sol {self._sol}, " + f"{self._hour:02d}:{self._minute:02d}:{self._second:02d} " + f"({self._body.value} / {self._epoch_type.value} epoch)" + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-314.pyc b/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ad3bbfd7c9ce4c5029d933d34a93e056c94576b GIT binary patch literal 152 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%TzxjKQ|Rf zGgk=%jn<_dByQyKCJ`va2&f;5hoxKMOa4ge+o490A*Sz7EDZxkGg28mTJm9Jm@> z4V)lX)oR-btO;BTT@6>mCx{+AN_6!oDRx&iy$$RTv)lDB*c!7VdOO$=W=HiX*d5I7 z&^y8IVs@t<1G}5qUAl6TBn~{o;oM9pmij??X>Cz~9Deb@k;3T7^Y2j`WVsCxc`PBXLavqY>gIwxz zd2w|)U#gm^>0+*wujc65YyoPkc@xSoH5OnnXDVw}hhgMOrE)b_%^L>AyQ;$1JOs%) z`I0C~;tNbR4zts9DE8j%C={?c48jQ4$vir+V_N(Uq5#e-WEy*KIx5PYV=KMcz{pkb z7(Qj!-uoBch53+hp4NoroO_Aho(&EwdgS)O;Xuaj8}HgYdp^)}y2J4oRxioe<|v%+ z^)_9pOZYkGUf0Yr_U|V~y7c{~_T5f9VfnSc0prwzt{M8a%Mx)tUER}Hk-lwv`UZq= zN*$DUnUIRAhxBk(O(~wzwt0Lc*x~B=uqboRy(HTvdKPDRlC}4h3W2ZkzCwp|4OT|< z*)eEk1uJW3DSNuX8p3P_lM7#Ujf$}4tU4Z+@6kp6B1 z8c;-?Zw93lr$fV#BovB}!ts;c2L^vEVHFMr4kGvsZd_(3esO3ik8cM9BX2Pf(E7WE zfw)gm+;ABD&=^{H59k3*4q}4O;Z2HA3%IB65BGjNH+^k(N`EEj3){~0C|>m)Oc0^z zyO_WpM=0_X`U6akV{!tM9!OdMK)nqLimsx2bir6k&YEtBO)#T%gykf`I5bP%hrN7&N2Cp4ckcXJx zg9Le1K$%p&VChpp70k853O)vmTKyJMH?`^}Q_7V;!&VKH(U)d*`_inyJi;D3vU2lZ zNoRwMcwH8BW=gsDD@JG};hL6YO0*{gs8>f;1?ts#`%n}0l7ws8mq>Z27d%Wr@|-K} zvHZmtm*5fgw8MqcbN~^N)XAn=&UO6#9r~o|ZS(t?2d9uu|Gx8Zh?5{jaz30wI_-VO z*V-QD)dqAeu&=Sc$$K7l{kFbya-Kw8+tW8F?prG4C8v739?7;fQP$A>5%Ea4dd3yl zbik7m5&YqQ`dCIy_T0@++&rs10^s^FXNWY0&e~n8E7jV62nRx)1+eeXRaHcBm2S zt;c$6+C(EcUQdpL@yT2*IldjA0J9dKsK+K6P{}?!P!j8vS!q13N^?t_5V>7HZz%j^ z&h72W$zr(jlZ?zkmm-Hp;e>(M4WvA9% z3ik%KOMMPuv3Hlc^6#(Tqaf%fjPO441pR+wxUNE|d-=O|p836e<=&kNdQcGdEIs5Cxa051 z4p-0qSnLle9Qm-i0#C!O2|)VvvZzztNwgQA?qp!hV`2T!-c!?zL}>l`YsdZ6O1J_tXA8>0T- z^ufX(VA1a!(*4;=I!-Ia3!|BoYk!$onon@z1OnFN=;C6WrHx7K~CG2CIR)(^{L1ZiTQBmjd-3U9fs*nX%Ze`n;Ty|Fk1R$4jCL%))|2K|$S2hH4 zS?u)o5dPY?Ptf9A$)Cnc*-MPkmdl)Wct@KK9|YOU-xaigT(+NiaJkIs-*+C4 zXd~{voDY}Foc6x+X>Bj%vi*#e%Vo~Be8=hpd~%FDP;}b)j?rnyP&@2;Z|(0dgFjsZ z+3$aK9_@S1LrQsiWKUlimZmgMz#G=1AVPJI^c=d#FHh?Ap3RN zvR??!0r&<%{+pT#T)$w4znu{Nc02MPk20P2HcT_aK$vYvr~-~KsqZXHhrK!=#I~&X zoYVpTxQ~s?etVrB7HS__E+z0~Qec@b6sqt-nh9;N38q-AorZViTsTC_?rOwOIYjBO zlSk#aU0R)<4tb=$FZruS{-XB)|}dUC9$%{C@y>XS2I)Fx*> zO`?GcG7xO1O>tw_UcS&5GQvpEneu=ID64a2LmRIE literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_planetary_time.cpython-314-pytest-9.0.3.pyc b/tests/__pycache__/test_planetary_time.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2841e79943633a6fd4fda6b1a0a1bbd896cd374b GIT binary patch literal 21504 zcmeHPZ)_CD72mzv`{z3wV{B}L9iKyJxdJx+12zGt0o&nB!5oV@LS*@L*b7{9zB}gD zN$e`B)#d}VA3~K#rBbA#Hmb0M6e1y|m8h!pOIx*4eg4B)E2{Kcn{P!3MM}T)z1iK_ zz4LN5*yl9StcHhj-&YRu+y*Ka8?uzm<4z?XX`e*E~UXB~ZjK0}4;?WWi)7%Ir z@&laA)4eTdpRhG?K|yvjsb`$B6VBKMTtS!Y3c6)?&?9^3Ir~6a&?|d`KG_#6m&@rf zVW1+oN!}E!lq+f8F|avUC08|ZqO+D0UA0`iQr;rEf%Z_kS}X(FOX(WX2Xr~5w~7@& zZ=!UqSPArIO4o^1KyRURy;u!&4W+kq}xTLr- zok}Jo#e>wC6qlpGARJAOO)8%LRC4t4&}2$d%0uyJLXx9oatPQ0Hf39%Rz-+NqfjnQ zdHV@T5~YUHXA{vkqOo}NLR_MU1H4jkNs=QHRvN(;jPlSP{KpMtV2A%l4UkN8VQ$3M z{SL>C!LbJJG=G^FdC}Gi_c3Gt9C;Ndk>m-I7@XmtX6MBu8IQ_otnGQ_<<`r|aS4*d zRJ64(IXW>eCFFE#&ONlsQW}n78cFFl7r{FGE zV;Rqce8zd5YKGoh({-@ru%-AgAF5DkbDFY^a97&H_U<6ZUFrL_cIwifP*TH{I%f!b zEpKb)M0Z%|;6=~59UZn#tp`xD!__)#Xsb>c`)00F%N&ILrN+D~)ft>R1%1GGYQ3(v zaI7}3+%}!t*qh6pgm#H`(;Zp!+w|**!iv0howwD#hshdx+95hbXV}ro=Nxqo!=0*| zspjYOW5ym@?bK`5_wdBH4)H15A)fSt9Q_8jPOXC+%Sv+0ky!EN_`EJXW4{h^^p@nf z!E$`Tke^PigB*P&Ic~5Vzokph*sp^e%S&?HNI8lwz{eE?@q%JcP3}`{DH(}&O!j|S z5lyEhBKvplN=ZExLpk>a(k+uxl$5>g?@^EJ z52S&AcLDx%GM>*~sQSyviDlKd6sUeYmY9&GoGS$QsX`^v(r7X@DHs z5%CN`D5H4#2F~=J3ipfWi3{^(7fEtF0v#YYwI>v(dVc|z;@0#MnIQgDyx101>}}>u zr#aK1X4Fd^FlV~c*ad!dBb%|t7JBDKpG2a%E)+{guzMm?5=lm&jg%l=P>3oWuLJC# z<{oTn{9x>tv0ub)*JlHVGl9eNn|h{$4?J75p5{4E^UYni`#u_&8Mr%{?K+<6IzI3D z-t^HWZ~41j*Scnf+Urr8TsxcxP3E;;&+_(!uQwS=#&!(CaR7S}r zNCG@TMNrkp>G-EgG)=I6rb#>bx`!MjH)Xn0FAfZBW14gvUNY;iNrR>g)*y8&tTS6_ z+9KDb>6m-;N?@PjhndzKV*0SbfI4R3Ypq}h(Did$*k+|=r|h7g_l5 zLC*&L+(Gs8&OH5`>DVDlE!aXoH?D*xoxBbDfa%S83pM@RC}*wdvT~Y!z7jvvxj{c) ziC56I=REz~N%eDQo__8cZq{$hRI`PK26yYyYu5Lger^Z-d|9BX zW;;vq2NbPtAKmC9t~Qa2$Yo(V-lE*cp^_A(VsU7M&ESD*)y06$Q!_hEyRoWdd#1 zwiQZPOs(uerB~ZT&YXkE5^flwFWtg0U1bRkZ6-s$36aql}waL+2l4A~}D83x=E1tK(^SD+y`bu)# zNI8mbFcp`_DnY{szXO+QKYS*HSYwz`^AMs^4v;WSu^}1;$zd2Z6g93LCVEtuSc$lf z8XdJ8P%7Aw)gVW-%IL^ZgO1NxK}UKCwZeWVLh-`y3t-0wwcCF_`LoGvZA+%MMFol3 zwqT|$xL6zfbLV31sac_VsiyHm&ushA1%K~iP4D!e0V`YXzVJ!lu2$^uqYO^_%WDi z1qdmuLyKRor3hi20iG=3parZmuEZcANK)IN518dH57rswECHlZPJ?wT@iQ0*ux=$@ z1_AS6os+`4Zk?ci>$EPe>&bop2rGm}@C2$+7ryv;g%W$0u8E zjS*>~pt0xCmt*lO$+7ryv{JJQS#DP+$MTXKbKrTAx<&8=Iu+U zp*UX~u81gJm19X8%Z-mHvK&PYjCx*h=~SHZR4f4weFb4d-h^+U>dBTo7q}^PA~KO+u6PW*9U~L+C^-#pM*8C3UBFJa~23~(Jk8yXnu7uDbZSMRC8p(DLR)H|38MjgXD=@^C> z135aX`@>Hdk;D2Z9)Y^4w6Q5LiuD1yC+pd}=-JB#=!bQ;Ubw^GX}kC8ABKN7ywH67 zZ+(A1@s|?|uZ?D3dp+~o>kIBU4%ikb2ERGaUUk6!`bg!G9>=FW-lG*o9k6}SdXqxb zh?-C%zQAEuV6<*4%EZP-1rdu}%4YVA(Vry@Ax57NBZRo0Ul!WSjBLQDZD1nx(IDcy zASu)HV*QO^(+@PPmpwJs-Yz-Q8_lZ;95$T3^*hwR8e`xfm=hXp*}moF)#;8L-;~u( z#jr^tYj^Ec1V>x8(I^LPgOKGF`sL!(Mz-Ip=yk$(l+M)BHzsjl#B@t2!tg@*Uf+^Bl*91z#fWxNepLHjKT4HyrJ zxkZCyr|)AymYd7TvBOa5jVhJ{COrqGub*Ql%mgqkc`Ijy?eF*Au)TK-i0j8P-tAdI z`z;9DukKfKiw4O~-^YS1HJqM+)A7dxX1TZam|NnXmgrwqf7y}&s z;M)-ao?DyRaq+VY^Y zAUf6^Iy=GHyi;_Q`k&*>o@yK1|L#)%f2IBJ#Cf2piky|=b`dn4l2J$$p5)*tSkQw8 z0}f6((9^kRN&v_09`b!)1t3+G2c!TstqLkB0ijd~pa*W>Rq?0p=_5 None: + assert Body.MARS.hours_per_sol == 25 + + +def test_jupiter_hours_per_sol() -> None: + assert Body.JUPITER.hours_per_sol == 10 + + +def test_hours_per_sol_equals_rounded_rotation() -> None: + for body in Body: + assert body.hours_per_sol == round(body.rotation_hours) + + +def test_all_bodies_have_positive_rotation() -> None: + for body in Body: + assert body.rotation_hours > 0 + + +def test_mars_sols_per_year() -> None: + assert Body.MARS.sols_per_year == 670 + + +def test_all_bodies_have_positive_sols_per_year() -> None: + for body in Body: + assert body.sols_per_year > 0 + + +def test_sols_per_year_derived_from_orbital_and_rotation() -> None: + for body in Body: + assert body.sols_per_year == round(body.orbital_hours / body.rotation_hours) diff --git a/tests/test_planetary_time.py b/tests/test_planetary_time.py new file mode 100644 index 0000000..663c896 --- /dev/null +++ b/tests/test_planetary_time.py @@ -0,0 +1,82 @@ +import pytest +from datetime import datetime, timezone, timedelta + +from planetarytime import Body, EpochType, PlanetaryTime +from planetarytime.exceptions import DatetimePrecedesEpochError, EpochUnavailableError +from planetarytime.epoch import get_epoch_date + + +def test_from_earth_at_epoch_is_year_zero_sol_zero() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + pt = PlanetaryTime.from_earth(epoch_dt, Body.MARS, EpochType.DISCOVERY) + assert pt.year == 0 + assert pt.sol == 0 + assert pt.hour == 0 + assert pt.minute == 0 + assert pt.second == 0 + + +def test_from_earth_one_sol_later() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + one_sol_later = epoch_dt + timedelta(hours=Body.MARS.hours_per_sol) + pt = PlanetaryTime.from_earth(one_sol_later, Body.MARS, EpochType.DISCOVERY) + assert pt.year == 0 + assert pt.sol == 1 + assert pt.hour == 0 + + +def test_from_earth_one_year_later() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + one_year_seconds = Body.MARS.sols_per_year * Body.MARS.hours_per_sol * 3600 + one_year_later = epoch_dt + timedelta(seconds=one_year_seconds) + pt = PlanetaryTime.from_earth(one_year_later, Body.MARS, EpochType.DISCOVERY) + assert pt.year == 1 + assert pt.sol == 0 + assert pt.hour == 0 + + +def test_from_earth_one_hour_later() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + one_hour_later = epoch_dt + timedelta(hours=1) + pt = PlanetaryTime.from_earth(one_hour_later, Body.MARS, EpochType.DISCOVERY) + assert pt.year == 0 + assert pt.sol == 0 + assert pt.hour == 1 + assert pt.minute == 0 + + +def test_from_earth_naive_datetime_treated_as_utc() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + naive = epoch_dt.replace(tzinfo=None) + pt = PlanetaryTime.from_earth(naive, Body.MARS, EpochType.DISCOVERY) + assert pt.year == 0 + assert pt.sol == 0 + + +def test_from_earth_before_epoch_raises() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + before_epoch = epoch_dt - timedelta(days=1) + with pytest.raises(DatetimePrecedesEpochError): + PlanetaryTime.from_earth(before_epoch, Body.MARS, EpochType.DISCOVERY) + + +def test_contact_epoch_unavailable_raises() -> None: + with pytest.raises(EpochUnavailableError): + PlanetaryTime.from_earth(datetime(2024, 1, 1, tzinfo=timezone.utc), Body.JUPITER, EpochType.CONTACT) + + +def test_str_contains_body_name_and_sol() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + pt = PlanetaryTime.from_earth(epoch_dt, Body.MARS, EpochType.DISCOVERY) + assert "Mars" in str(pt) + assert "Sol" in str(pt) + assert "Year" in str(pt) + + +def test_repr_contains_year_and_sol() -> None: + epoch_dt = get_epoch_date(Body.MARS, EpochType.DISCOVERY) + pt = PlanetaryTime.from_earth(epoch_dt, Body.MARS, EpochType.DISCOVERY) + r = repr(pt) + assert "PlanetaryTime(" in r + assert "year=" in r + assert "sol=" in r diff --git a/zkouska.py b/zkouska.py new file mode 100644 index 0000000..313a5b6 --- /dev/null +++ b/zkouska.py @@ -0,0 +1,7 @@ +import planetarytime as pt +import datetime + +now = datetime.datetime.now(datetime.timezone.utc) +mars = pt.PlanetaryTime.from_earth(now, pt.Body.VENUS) + +print(mars) \ No newline at end of file