satnogs-predict 0.2__tar.gz → 0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {satnogs_predict-0.2 → satnogs_predict-0.3}/PKG-INFO +4 -3
  2. {satnogs_predict-0.2 → satnogs_predict-0.3}/README.md +3 -2
  3. {satnogs_predict-0.2 → satnogs_predict-0.3}/docs/docs.md +15 -0
  4. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/__init__.py +2 -0
  5. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/orbit.py +3 -0
  6. satnogs_predict-0.3/src/satnogs_predict/tle.py +107 -0
  7. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict.egg-info/PKG-INFO +4 -3
  8. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict.egg-info/SOURCES.txt +2 -0
  9. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/domain/test_orbit.py +7 -0
  10. satnogs_predict-0.3/tests/test_tle.py +53 -0
  11. {satnogs_predict-0.2 → satnogs_predict-0.3}/.gitignore +0 -0
  12. {satnogs_predict-0.2 → satnogs_predict-0.3}/.gitlab-ci.yml +0 -0
  13. {satnogs_predict-0.2 → satnogs_predict-0.3}/.pre-commit-config.yaml +0 -0
  14. {satnogs_predict-0.2 → satnogs_predict-0.3}/.python-version +0 -0
  15. {satnogs_predict-0.2 → satnogs_predict-0.3}/CONTRIBUTING.md +0 -0
  16. {satnogs_predict-0.2 → satnogs_predict-0.3}/LICENSE +0 -0
  17. {satnogs_predict-0.2 → satnogs_predict-0.3}/pyproject.toml +0 -0
  18. {satnogs_predict-0.2 → satnogs_predict-0.3}/setup.cfg +0 -0
  19. {satnogs_predict-0.2 → satnogs_predict-0.3}/setup.py +0 -0
  20. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/constraints/__init__.py +0 -0
  21. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/constraints/constraints.py +0 -0
  22. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/core/__init__.py +0 -0
  23. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/core/engine.py +0 -0
  24. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/__init__.py +0 -0
  25. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/constraints.py +0 -0
  26. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/geometry.py +0 -0
  27. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/observer.py +0 -0
  28. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/planner.py +0 -0
  29. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/time.py +0 -0
  30. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/validation.py +0 -0
  31. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/domain/window.py +0 -0
  32. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/planning/__init__.py +0 -0
  33. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/planning/planner.py +0 -0
  34. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/propagation/__init__.py +0 -0
  35. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/propagation/propagator.py +0 -0
  36. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict/py.typed +0 -0
  37. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
  38. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict.egg-info/requires.txt +0 -0
  39. {satnogs_predict-0.2 → satnogs_predict-0.3}/src/satnogs_predict.egg-info/top_level.txt +0 -0
  40. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/__init__.py +0 -0
  41. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/conftest.py +0 -0
  42. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/constraints/test_constraints.py +0 -0
  43. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/core/test_engine.py +0 -0
  44. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/domain/test_planner_domain.py +0 -0
  45. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/domain/test_required_fields.py +0 -0
  46. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/domain/test_time.py +0 -0
  47. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/domain/test_window.py +0 -0
  48. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/helpers.py +0 -0
  49. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/planning/test_planner.py +0 -0
  50. {satnogs_predict-0.2 → satnogs_predict-0.3}/tests/propagation/test_propagator.py +0 -0
  51. {satnogs_predict-0.2 → satnogs_predict-0.3}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: A package for calculating passes and observation windows for satellites.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -13,8 +13,7 @@ Dynamic: license-file
13
13
  [![Pipeline Status](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/pipeline.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
14
14
  [![Coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
15
15
  [![PyPI version](https://img.shields.io/pypi/v/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
16
- [![Python Versions](https://img.shields.io/pypi/pyversions/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
17
-
16
+ ![Python Version](https://img.shields.io/badge/dynamic/toml?url=https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/raw/fb6964df5384da584c1dbb92ac157104c8a3948d/pyproject.toml&query=project.requires-python&label=python)
18
17
  ---
19
18
 
20
19
  ## 🚀 Overview
@@ -45,6 +44,8 @@ pip install satnogs-predict
45
44
  from satnogs_predict import find_observation_windows
46
45
  ```
47
46
 
47
+ For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
48
+
48
49
  ## License
49
50
 
50
51
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -3,8 +3,7 @@
3
3
  [![Pipeline Status](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/pipeline.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
4
4
  [![Coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
5
5
  [![PyPI version](https://img.shields.io/pypi/v/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
6
- [![Python Versions](https://img.shields.io/pypi/pyversions/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
7
-
6
+ ![Python Version](https://img.shields.io/badge/dynamic/toml?url=https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/raw/fb6964df5384da584c1dbb92ac157104c8a3948d/pyproject.toml&query=project.requires-python&label=python)
8
7
  ---
9
8
 
10
9
  ## 🚀 Overview
@@ -35,6 +34,8 @@ pip install satnogs-predict
35
34
  from satnogs_predict import find_observation_windows
36
35
  ```
37
36
 
37
+ For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
38
+
38
39
  ## License
39
40
 
40
41
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -44,6 +44,7 @@ from satnogs_predict import (
44
44
  get_and_validate_pass_from_range,
45
45
  check_observation_density_limit_respected,
46
46
  has_range_list_overlap,
47
+ generate_fake_tle,
47
48
 
48
49
  MaxDurationConstraint,
49
50
  MinCulminationConstraint,
@@ -86,6 +87,20 @@ satellite = Satellite.from_tle(
86
87
  )
87
88
  ```
88
89
 
90
+ For testing or development, you can also generate a synthetic TLE:
91
+
92
+ ```python
93
+ from datetime import datetime, timezone
94
+
95
+ fake_tle = generate_fake_tle(
96
+ latitude=37.983810,
97
+ longitude=23.727539,
98
+ date=datetime(2026, 3, 6, 12, 0, 0, tzinfo=timezone.utc),
99
+ )
100
+
101
+ line0, line1, line2 = fake_tle.to_list()
102
+ ```
103
+
89
104
 
90
105
  ## 3.2 - Observer
91
106
 
@@ -3,7 +3,9 @@ from .constraints import * # noqa: F401,F403
3
3
  from .core.engine import find_observation_windows, get_and_validate_pass_from_range
4
4
  from .domain import * # noqa: F401,F403
5
5
  from .planning.planner import check_observation_density_limit_respected, has_range_list_overlap
6
+ from .tle import generate_fake_tle
6
7
 
7
8
  __all__ = list(domain.__all__) + list(constraints.__all__)
8
9
  __all__ += ["check_observation_density_limit_respected", "has_range_list_overlap"]
9
10
  __all__ += ["find_observation_windows", "get_and_validate_pass_from_range"]
11
+ __all__ += ["generate_fake_tle"]
@@ -15,6 +15,9 @@ class TLE:
15
15
  def __post_init__(self) -> None:
16
16
  ensure_required_fields_not_none(self)
17
17
 
18
+ def to_list(self) -> list[str]:
19
+ return [self.line0, self.line1, self.line2]
20
+
18
21
 
19
22
  class OrbitRepresentation(IntEnum):
20
23
  TLE = 0
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone
5
+
6
+ from skyfield.api import load # type: ignore[import-untyped]
7
+
8
+ from satnogs_predict.domain.orbit import TLE
9
+
10
+ _ts = load.timescale()
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FakeTLEParameters:
15
+ name: str = "FAKESAT-1"
16
+ catalog_num: int = 0
17
+ classifier: str = "U"
18
+ cospar_id: str = "00000A"
19
+ mean_motion_first: str = ".00000000"
20
+ mean_motion_second: str = "00000-0"
21
+ drag_term: str = "50000-4"
22
+ set_number: str = "0"
23
+ inclination: str = "90.0000"
24
+ eccentricity: str = "0001000"
25
+ argument_perigee: str = "000.0000"
26
+ mean_motion: str = "15.50000000"
27
+ revolutions: str = "0"
28
+
29
+
30
+ DEFAULT_FAKE_TLE_PARAMETERS = FakeTLEParameters()
31
+
32
+
33
+ def _tle_checksum(line_without_checksum: str) -> int:
34
+ total = 0
35
+ for char in line_without_checksum:
36
+ if char.isdigit():
37
+ total += int(char)
38
+ elif char == "-":
39
+ total += 1
40
+ return total % 10
41
+
42
+
43
+ def _epoch_day_of_year_fraction(epoch: datetime) -> float:
44
+ year_start = datetime(epoch.year, 1, 1, tzinfo=timezone.utc)
45
+ delta = epoch - year_start
46
+ day = delta.days + 1
47
+ day_fraction = (delta.seconds + delta.microseconds / 1_000_000) / 86_400
48
+ return day + day_fraction
49
+
50
+
51
+ def generate_fake_tle(
52
+ latitude: float,
53
+ longitude: float,
54
+ date: datetime,
55
+ params: FakeTLEParameters = DEFAULT_FAKE_TLE_PARAMETERS,
56
+ ) -> TLE:
57
+ """
58
+ Generate a deterministic, synthetic TLE from observer coordinates and an epoch.
59
+
60
+ The result is intended for testing and development scenarios where a plausible
61
+ but not physically accurate TLE is sufficient.
62
+ """
63
+ if date.tzinfo is None:
64
+ raise ValueError("date must be timezone-aware")
65
+
66
+ epoch = date.astimezone(timezone.utc)
67
+ skyfield_time = _ts.from_datetime(epoch)
68
+
69
+ # Derived values
70
+ right_ascension = (skyfield_time.gmst * 15.0 + longitude) % 360.0
71
+ mean_anomaly = latitude % 360.0
72
+
73
+ epoch_year = epoch.year
74
+ epoch_day = _epoch_day_of_year_fraction(epoch)
75
+
76
+ line0 = params.name.ljust(24)
77
+
78
+ line1_no_checksum = (
79
+ "1 {:5.5}{:.1} {:8.8} {:.2}{:012.8f} {:>10.10} {:>8.8} {:>8.8} 0 {:>4.4}".format(
80
+ f"{params.catalog_num:05d}",
81
+ params.classifier,
82
+ params.cospar_id,
83
+ str(epoch_year),
84
+ epoch_day,
85
+ params.mean_motion_first,
86
+ params.mean_motion_second,
87
+ params.drag_term,
88
+ params.set_number,
89
+ )
90
+ )
91
+
92
+ line2_no_checksum = (
93
+ "2 {:>5.5} {:>8.8} {:>8.4f} {:>7.7} {:>8.8} {:>8.4f} {:>11.11}{:>5.5}".format(
94
+ f"{params.catalog_num:05d}",
95
+ params.inclination,
96
+ right_ascension,
97
+ params.eccentricity,
98
+ params.argument_perigee,
99
+ mean_anomaly,
100
+ params.mean_motion,
101
+ params.revolutions,
102
+ )
103
+ )
104
+
105
+ line1 = f"{line1_no_checksum}{_tle_checksum(line1_no_checksum)}"
106
+ line2 = f"{line2_no_checksum}{_tle_checksum(line2_no_checksum)}"
107
+ return TLE(line0=line0, line1=line1, line2=line2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: A package for calculating passes and observation windows for satellites.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -13,8 +13,7 @@ Dynamic: license-file
13
13
  [![Pipeline Status](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/pipeline.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
14
14
  [![Coverage](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/badges/main/coverage.svg)](https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/pipelines)
15
15
  [![PyPI version](https://img.shields.io/pypi/v/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
16
- [![Python Versions](https://img.shields.io/pypi/pyversions/satnogs-predict.svg)](https://pypi.org/project/satnogs-predict/)
17
-
16
+ ![Python Version](https://img.shields.io/badge/dynamic/toml?url=https://gitlab.com/librespacefoundation/satnogs/satnogs-predict/-/raw/fb6964df5384da584c1dbb92ac157104c8a3948d/pyproject.toml&query=project.requires-python&label=python)
18
17
  ---
19
18
 
20
19
  ## 🚀 Overview
@@ -45,6 +44,8 @@ pip install satnogs-predict
45
44
  from satnogs_predict import find_observation_windows
46
45
  ```
47
46
 
47
+ For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
48
+
48
49
  ## License
49
50
 
50
51
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -11,6 +11,7 @@ uv.lock
11
11
  docs/docs.md
12
12
  src/satnogs_predict/__init__.py
13
13
  src/satnogs_predict/py.typed
14
+ src/satnogs_predict/tle.py
14
15
  src/satnogs_predict.egg-info/PKG-INFO
15
16
  src/satnogs_predict.egg-info/SOURCES.txt
16
17
  src/satnogs_predict.egg-info/dependency_links.txt
@@ -36,6 +37,7 @@ src/satnogs_predict/propagation/propagator.py
36
37
  tests/__init__.py
37
38
  tests/conftest.py
38
39
  tests/helpers.py
40
+ tests/test_tle.py
39
41
  tests/constraints/test_constraints.py
40
42
  tests/core/test_engine.py
41
43
  tests/domain/test_orbit.py
@@ -3,6 +3,13 @@ from __future__ import annotations
3
3
  from satnogs_predict.domain.orbit import TLE, OrbitRepresentation, Satellite
4
4
 
5
5
 
6
+ class TestTle:
7
+ def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
8
+ tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
9
+
10
+ assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
11
+
12
+
6
13
  class TestSatelliteFromTle:
7
14
  def test_given_no_identifier_when_created_then_uses_line0(self) -> None:
8
15
  line0 = "ISS (ZARYA)"
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ import pytest
6
+ from skyfield.api import EarthSatellite, load # type: ignore[import-untyped]
7
+
8
+ from satnogs_predict import generate_fake_tle
9
+
10
+ ts = load.timescale()
11
+
12
+
13
+ def _checksum(line_without_checksum: str) -> int:
14
+ total = 0
15
+ for char in line_without_checksum:
16
+ if char.isdigit():
17
+ total += int(char)
18
+ elif char == "-":
19
+ total += 1
20
+ return total % 10
21
+
22
+
23
+ class TestGenerateFakeTle:
24
+ def test_given_valid_input_when_generated_then_returns_parseable_tle(self) -> None:
25
+ tle = generate_fake_tle(
26
+ latitude=37.9838,
27
+ longitude=23.7275,
28
+ date=datetime(2026, 3, 6, 12, 0, 0, tzinfo=timezone.utc),
29
+ )
30
+
31
+ assert len(tle.line1) == 69
32
+ assert len(tle.line2) == 69
33
+ assert tle.line1[-1] == str(_checksum(tle.line1[:-1]))
34
+ assert tle.line2[-1] == str(_checksum(tle.line2[:-1]))
35
+
36
+ sat = EarthSatellite(tle.line1, tle.line2, tle.line0, ts)
37
+ assert sat.name == tle.line0.strip()
38
+
39
+ def test_given_same_input_when_generated_twice_then_result_is_deterministic(self) -> None:
40
+ date = datetime(2026, 3, 6, 12, 0, 0, tzinfo=timezone.utc)
41
+
42
+ tle1 = generate_fake_tle(latitude=10.0, longitude=20.0, date=date)
43
+ tle2 = generate_fake_tle(latitude=10.0, longitude=20.0, date=date)
44
+
45
+ assert tle1 == tle2
46
+
47
+ def test_given_naive_date_when_generated_then_raises(self) -> None:
48
+ with pytest.raises(ValueError, match="timezone-aware"):
49
+ generate_fake_tle(
50
+ latitude=0.0,
51
+ longitude=0.0,
52
+ date=datetime(2026, 3, 6, 12, 0, 0),
53
+ )
File without changes
File without changes
File without changes
File without changes
File without changes