Initial commit

This commit is contained in:
2026-04-16 08:49:32 +02:00
commit ff51b802e5
27 changed files with 1312 additions and 0 deletions

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

83
AGENTS.md Normal file
View File

@@ -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

10
CLAUDE.md Normal file
View File

@@ -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`

260
DESIGN_DOCUMENT_MODULE.md Normal file
View File

@@ -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("<package_name>") # suppress all
logger.add(sys.stderr, filter={"<package_name>": "WARNING"}) # WARNING and above only
logger.enable("<package_name>") # 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/<package>/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_<action>_<context>`
- 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 <pkg> # Add runtime dependency
poetry add --group dev <pkg> # Add dev dependency
poetry remove <pkg> # Remove dependency
poetry run <cmd> # 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/<package>/__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`.

88
PROJECT.md Normal file
View File

@@ -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

View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# PlanetaryTime
<!-- TODO: short description of the library -->
## 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("<package_name>")
```
To re-enable them:
```python
logger.enable("<package_name>")
```
### 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={"<package_name>": "WARNING"})
```

370
poetry.lock generated Normal file
View File

@@ -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"

26
pyproject.toml Normal file
View File

@@ -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)"
]

View File

@@ -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",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

55
src/planetarytime/body.py Normal file
View File

@@ -0,0 +1,55 @@
from enum import Enum
class Body(Enum):
"""Planetary body in the Solar System."""
MERCURY = "Mercury"
VENUS = "Venus"
MARS = "Mars"
JUPITER = "Jupiter"
SATURN = "Saturn"
URANUS = "Uranus"
NEPTUNE = "Neptune"
@property
def rotation_hours(self) -> 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,
}

View File

@@ -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

View File

@@ -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."""

View File

@@ -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)"
)

0
tests/__init__.py Normal file
View File

Binary file not shown.

33
tests/test_body.py Normal file
View File

@@ -0,0 +1,33 @@
from planetarytime import Body
def test_mars_hours_per_sol() -> 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)

View File

@@ -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

7
zkouska.py Normal file
View File

@@ -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)