satnogs-predict 0.6__tar.gz → 1.0__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 (60) hide show
  1. satnogs_predict-1.0/CHANGELOG.md +19 -0
  2. {satnogs_predict-0.6 → satnogs_predict-1.0}/PKG-INFO +3 -1
  3. {satnogs_predict-0.6 → satnogs_predict-1.0}/README.md +2 -0
  4. {satnogs_predict-0.6 → satnogs_predict-1.0}/docs/docs.md +21 -22
  5. satnogs_predict-1.0/src/satnogs_predict/domain/observer.py +35 -0
  6. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/orbit.py +37 -12
  7. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/window.py +3 -3
  8. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/propagation/propagator.py +13 -14
  9. satnogs_predict-1.0/src/satnogs_predict/utils.py +7 -0
  10. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/PKG-INFO +3 -1
  11. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/SOURCES.txt +4 -0
  12. satnogs_predict-1.0/src/satnogs_predict.egg-info/scm_file_list.json +52 -0
  13. satnogs_predict-1.0/src/satnogs_predict.egg-info/scm_version.json +8 -0
  14. satnogs_predict-1.0/tests/domain/test_observer.py +59 -0
  15. satnogs_predict-1.0/tests/domain/test_orbit.py +131 -0
  16. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_required_fields.py +0 -11
  17. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/helpers.py +24 -11
  18. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/propagation/test_propagator.py +6 -10
  19. satnogs_predict-0.6/src/satnogs_predict/domain/observer.py +0 -22
  20. satnogs_predict-0.6/tests/domain/test_observer.py +0 -44
  21. satnogs_predict-0.6/tests/domain/test_orbit.py +0 -146
  22. {satnogs_predict-0.6 → satnogs_predict-1.0}/.gitignore +0 -0
  23. {satnogs_predict-0.6 → satnogs_predict-1.0}/.gitlab-ci.yml +0 -0
  24. {satnogs_predict-0.6 → satnogs_predict-1.0}/.pre-commit-config.yaml +0 -0
  25. {satnogs_predict-0.6 → satnogs_predict-1.0}/.python-version +0 -0
  26. {satnogs_predict-0.6 → satnogs_predict-1.0}/CONTRIBUTING.md +0 -0
  27. {satnogs_predict-0.6 → satnogs_predict-1.0}/LICENSE +0 -0
  28. {satnogs_predict-0.6 → satnogs_predict-1.0}/pyproject.toml +0 -0
  29. {satnogs_predict-0.6 → satnogs_predict-1.0}/setup.cfg +0 -0
  30. {satnogs_predict-0.6 → satnogs_predict-1.0}/setup.py +0 -0
  31. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/__init__.py +0 -0
  32. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/constraints/__init__.py +0 -0
  33. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/constraints/constraints.py +0 -0
  34. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/core/__init__.py +0 -0
  35. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/core/engine.py +0 -0
  36. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/__init__.py +0 -0
  37. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/constraints.py +0 -0
  38. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/geometry.py +0 -0
  39. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/planner.py +0 -0
  40. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/time.py +0 -0
  41. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/domain/validation.py +0 -0
  42. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/planning/__init__.py +0 -0
  43. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/planning/planner.py +0 -0
  44. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/propagation/__init__.py +0 -0
  45. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/py.typed +0 -0
  46. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict/tle.py +0 -0
  47. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
  48. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/requires.txt +0 -0
  49. {satnogs_predict-0.6 → satnogs_predict-1.0}/src/satnogs_predict.egg-info/top_level.txt +0 -0
  50. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/__init__.py +0 -0
  51. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/conftest.py +0 -0
  52. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/constraints/test_constraints.py +0 -0
  53. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/core/test_engine.py +0 -0
  54. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_planner_domain.py +0 -0
  55. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_time.py +0 -0
  56. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/domain/test_window.py +0 -0
  57. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/integration/test_fake_tle_integration.py +0 -0
  58. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/planning/test_planner.py +0 -0
  59. {satnogs_predict-0.6 → satnogs_predict-1.0}/tests/test_tle.py +0 -0
  60. {satnogs_predict-0.6 → satnogs_predict-1.0}/uv.lock +0 -0
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 1.0
4
+
5
+ ### Breaking Changes
6
+
7
+ - `Satellite.identifier` no longer defaults to a generated UUID. It now defaults to an empty string and is treated as caller metadata.
8
+ - `Observer.identifier` no longer defaults to a generated UUID. It now defaults to an empty string and is treated as caller metadata.
9
+ - Skyfield satellite and observer caches **no longer use identifiers as cache keys**. Satellite cache identity is based on orbital data, and observer cache identity is based on latitude, longitude, and elevation.
10
+ - `OMM.fields` is now stored as a read-only mapping. Use `OMM.to_dict()` when a mutable dictionary is needed.
11
+
12
+ ### Added
13
+
14
+ - Added `cache_key` properties for `TLE`, `OMM`, `Orbit`, `Satellite`, and `Observer`.
15
+ - Added a shared internal `cache_digest()` helper.
16
+
17
+ ### Improved
18
+
19
+ - It is no longer the caller's responsibility to manage cache by using identifiers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.6
3
+ Version: 1.0
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
@@ -46,6 +46,8 @@ from satnogs_predict import find_observation_windows
46
46
 
47
47
  For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
48
48
 
49
+ For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
50
+
49
51
  ## License
50
52
 
51
53
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -36,6 +36,8 @@ from satnogs_predict import find_observation_windows
36
36
 
37
37
  For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
38
38
 
39
+ For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
40
+
39
41
  ## License
40
42
 
41
43
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -20,10 +20,9 @@
20
20
  - [6 - Finding passes](#6---finding-passes)
21
21
  - [6.1 - Constraints](#61---constraints)
22
22
  - [6.2 - Windows returned](#62---windows-returned)
23
- - [6.3 - Implementation Details](#63---implementation-details)
24
- - [6.4 - Full workflow for finding passes](#64---full-workflow-for-finding-passes)
25
- - [6.5 - Sampling point-in-time geometry](#65---sampling-point-in-time-geometry)
26
- - [6.6 - Full workflow for validating time ranges that belong to passes](#66---full-workflow-for-validating-time-ranges-that-belong-to-passes)
23
+ - [6.3 - Full workflow for finding passes](#63---full-workflow-for-finding-passes)
24
+ - [6.4 - Sampling point-in-time geometry](#64---sampling-point-in-time-geometry)
25
+ - [6.5 - Full workflow for validating time ranges that belong to passes](#65---full-workflow-for-validating-time-ranges-that-belong-to-passes)
27
26
 
28
27
 
29
28
  # 1. Installation
@@ -102,8 +101,12 @@ satellite = Satellite.from_omm(
102
101
  )
103
102
  ```
104
103
 
105
- The `identifier` is optional and defaults to a random UUID. Provide an explicit
106
- identifier when you need a stable public value or cache key.
104
+ The `identifier` is optional metadata and defaults to an empty string. It is copied
105
+ to produced `ObservationWindow.sat_identifier` values so callers can conveniently connect windows back to their own satellite records.
106
+
107
+ Because the underlying skyfield object EarthSatellite is computationally expensive to create, the EarthSatellite of the last-used satellite is cached. Cache identity is based on orbital state: TLE `line1`/`line2`, or normalized OMM fields.
108
+
109
+ OMM field data are copied when the `OMM` object is created. The stored mapping is read-only, and `to_dict()` returns a mutable copy.
107
110
 
108
111
  For testing or development, you can also generate a synthetic TLE:
109
112
 
@@ -133,8 +136,11 @@ station = Observer(
133
136
  )
134
137
  ```
135
138
 
136
- The `identifier` is optional here as well and defaults to a random UUID. Provide one
137
- when you need a stable public value or cache key.
139
+ The `identifier` is optional metadata and defaults to an empty string. It is copied
140
+ to produced `ObservationWindow.observer_identifier` for convenience.
141
+
142
+ Because the underlying skyfield object Topos that represents an observer is computationally expensive, the Topos of the last-used observer is cached.
143
+ Observer cache identity depends on `lat_deg`, `lon_deg` and `elevation_meters`. The `horizon_deg` value is not part of the Topos object so it doesn't affect caching.
138
144
 
139
145
 
140
146
  ## 3.3 - Time Modeling
@@ -248,13 +254,14 @@ class ObservationWindow:
248
254
  A schedulable observation window.
249
255
  """
250
256
 
251
- sat_identifier: str
252
- observer_identifier: str
253
-
254
257
  start_sample: GeometrySample
255
258
  end_sample: GeometrySample
256
259
  max_altitude_sample: GeometrySample
257
260
 
261
+ # Caller metadata copied from Satellite.identifier and Observer.identifier.
262
+ sat_identifier: str = ""
263
+ observer_identifier: str = ""
264
+
258
265
  # The altitude has multiple peaks during the window (None means unknown)
259
266
  has_multiple_peaks: bool | None = None
260
267
 
@@ -404,15 +411,7 @@ For the helper `get_and_validate_pass_from_range`, constraints are validated via
404
411
 
405
412
  The function returns a tuple of observation window lists. The first element of the tuple is the list of valid windows, and the second is the list of windows that were rejected due to constraints.
406
413
 
407
- ## 6.3 - Implementation Details
408
-
409
- Internally, satellite & observer are converted to Skyfield's types, which is computationally expensive. For this reason, the last satellite and the last observer used is cached, differentiated by the `identifier` field.
410
-
411
- This way, repeated consecutive calls of `find_observation_windows` with the same satellite, or with the same `observer` reuse the objects under the hood.
412
-
413
- If you need to clear these caches explicitly, the public helpers `clear_satellite_cache()` and `clear_observer_cache()` are available.
414
-
415
- ## 6.4 - Full workflow for finding passes
414
+ ## 6.3 - Full workflow for finding passes
416
415
 
417
416
  High-level process:
418
417
 
@@ -484,7 +483,7 @@ valid_windows, rejected_windows = find_observation_windows(
484
483
  )
485
484
  ```
486
485
 
487
- ## 6.5 - Sampling point-in-time geometry
486
+ ## 6.4 - Sampling point-in-time geometry
488
487
 
489
488
  For point-in-time queries, the public API also provides helpers for sampling a satellite relative to an observer at a single `Instant`.
490
489
 
@@ -506,7 +505,7 @@ range_sample = get_range(
506
505
  - `get_range()` returns a `RangeSample` with range in meters and range rate in meters per second.
507
506
 
508
507
 
509
- ## 6.6 - Full workflow for validating time ranges that belong to passes
508
+ ## 6.5 - Full workflow for validating time ranges that belong to passes
510
509
 
511
510
  - Given a time range, we can validate whether it belongs to a single pass and whether it satisfies constraints.
512
511
 
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from satnogs_predict.utils import cache_digest
6
+
7
+ from .validation import ensure_required_fields_not_none
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Observer:
12
+ """Observer to calculate passes over e.g. a ground station"""
13
+
14
+ lat_deg: float
15
+ lon_deg: float
16
+ elevation_meters: int
17
+ horizon_deg: float
18
+ identifier: str = ""
19
+ _cache_key: str = field(init=False, repr=False, compare=False, default="")
20
+
21
+ def __post_init__(self) -> None:
22
+ ensure_required_fields_not_none(self)
23
+ object.__setattr__(
24
+ self,
25
+ "_cache_key",
26
+ cache_digest(
27
+ str(self.lat_deg),
28
+ str(self.lon_deg),
29
+ str(self.elevation_meters),
30
+ ),
31
+ )
32
+
33
+ @property
34
+ def cache_key(self) -> str:
35
+ return self._cache_key
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  from collections.abc import Mapping
4
4
  from dataclasses import dataclass, field
5
5
  from enum import IntEnum
6
- from uuid import uuid4
6
+ from types import MappingProxyType
7
+
8
+ from satnogs_predict.utils import cache_digest
7
9
 
8
10
  from .validation import ensure_required_fields_not_none
9
11
 
@@ -13,24 +15,41 @@ class TLE:
13
15
  line0: str
14
16
  line1: str
15
17
  line2: str
18
+ _cache_key: str = field(init=False, repr=False, compare=False, default="")
16
19
 
17
20
  def __post_init__(self) -> None:
18
21
  ensure_required_fields_not_none(self)
22
+ object.__setattr__(self, "_cache_key", cache_digest(self.line1, self.line2))
19
23
 
20
24
  def to_list(self) -> list[str]:
21
25
  return [self.line0, self.line1, self.line2]
22
26
 
27
+ @property
28
+ def cache_key(self) -> str:
29
+ return self._cache_key
30
+
23
31
 
24
32
  @dataclass(frozen=True, slots=True)
25
33
  class OMM:
26
- fields: dict[str, str]
34
+ fields: Mapping[str, str]
35
+ _cache_key: str = field(init=False, repr=False, compare=False, default="")
27
36
 
28
37
  def __post_init__(self) -> None:
29
38
  ensure_required_fields_not_none(self)
39
+ fields = MappingProxyType(dict(self.fields))
40
+ object.__setattr__(self, "fields", fields)
41
+ parts: list[str] = []
42
+ for key, value in sorted(fields.items()):
43
+ parts.extend((key, value))
44
+ object.__setattr__(self, "_cache_key", cache_digest(*parts))
30
45
 
31
46
  def to_dict(self) -> dict[str, str]:
32
47
  return dict(self.fields)
33
48
 
49
+ @property
50
+ def cache_key(self) -> str:
51
+ return self._cache_key
52
+
34
53
 
35
54
  class OrbitRepresentation(IntEnum):
36
55
  TLE = 0
@@ -41,27 +60,37 @@ class OrbitRepresentation(IntEnum):
41
60
  class Orbit:
42
61
  representation: OrbitRepresentation
43
62
  data: TLE | OMM
63
+ _cache_key: str = field(init=False, repr=False, compare=False, default="")
44
64
 
45
65
  def __post_init__(self) -> None:
46
66
  ensure_required_fields_not_none(self)
67
+ object.__setattr__(self, "_cache_key", self.data.cache_key)
68
+
69
+ @property
70
+ def cache_key(self) -> str:
71
+ return self._cache_key
47
72
 
48
73
  @classmethod
49
74
  def from_omm(
50
75
  cls,
51
76
  fields: Mapping[str, str],
52
77
  ):
53
- return cls(representation=OrbitRepresentation.OMM, data=OMM(fields=dict(fields)))
78
+ return cls(representation=OrbitRepresentation.OMM, data=OMM(fields=fields))
54
79
 
55
80
 
56
81
  @dataclass(frozen=True, slots=True)
57
82
  class Satellite:
58
83
  orbit: Orbit
59
- identifier: str = field(default_factory=lambda: str(uuid4()))
84
+ identifier: str = ""
85
+ _cache_key: str = field(init=False, repr=False, compare=False, default="")
60
86
 
61
87
  def __post_init__(self) -> None:
62
- if self.identifier is None:
63
- object.__setattr__(self, "identifier", str(uuid4()))
64
88
  ensure_required_fields_not_none(self)
89
+ object.__setattr__(self, "_cache_key", self.orbit.cache_key)
90
+
91
+ @property
92
+ def cache_key(self) -> str:
93
+ return self._cache_key
65
94
 
66
95
  @classmethod
67
96
  def from_tle(
@@ -69,21 +98,17 @@ class Satellite:
69
98
  line0: str,
70
99
  line1: str,
71
100
  line2: str,
72
- identifier: str | None = None,
101
+ identifier: str = "",
73
102
  ):
74
103
  tle = TLE(line0=line0, line1=line1, line2=line2)
75
104
  orbit = Orbit(representation=OrbitRepresentation.TLE, data=tle)
76
- if identifier is None:
77
- return cls(orbit=orbit)
78
105
  return cls(orbit=orbit, identifier=identifier)
79
106
 
80
107
  @classmethod
81
108
  def from_omm(
82
109
  cls,
83
110
  fields: Mapping[str, str],
84
- identifier: str | None = None,
111
+ identifier: str = "",
85
112
  ):
86
113
  orbit = Orbit.from_omm(fields)
87
- if identifier is None:
88
- return cls(orbit=orbit)
89
114
  return cls(orbit=orbit, identifier=identifier)
@@ -19,13 +19,13 @@ class ObservationWindow:
19
19
  A schedulable observation window.
20
20
  """
21
21
 
22
- sat_identifier: str
23
- observer_identifier: str
24
-
25
22
  start_sample: GeometrySample
26
23
  end_sample: GeometrySample
27
24
  max_altitude_sample: GeometrySample
28
25
 
26
+ sat_identifier: str = ""
27
+ observer_identifier: str = ""
28
+
29
29
  # The altitude does has multiple peaks during the window (None means unknown)
30
30
  has_multiple_peaks: bool | None = None
31
31
 
@@ -65,20 +65,20 @@ def _instant_from_skyfield_time(t: Time):
65
65
 
66
66
 
67
67
  _last_sat: EarthSatellite | None = None
68
- _last_sat_identifier: str | None = None
68
+ _last_sat_cache_key: str | None = None
69
69
 
70
70
 
71
71
  def clear_satellite_cache() -> None:
72
- global _last_sat, _last_sat_identifier
72
+ global _last_sat, _last_sat_cache_key
73
73
 
74
74
  _last_sat = None
75
- _last_sat_identifier = None
75
+ _last_sat_cache_key = None
76
76
 
77
77
 
78
78
  def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
79
- global _last_sat, _last_sat_identifier
79
+ global _last_sat, _last_sat_cache_key
80
80
 
81
- if _last_sat_identifier == satellite.identifier and _last_sat is not None:
81
+ if _last_sat_cache_key == satellite.cache_key and _last_sat is not None:
82
82
  return _last_sat
83
83
 
84
84
  match satellite.orbit.representation:
@@ -87,8 +87,7 @@ def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
87
87
  skyfield_sat = EarthSatellite(
88
88
  tle.line1,
89
89
  tle.line2,
90
- satellite.identifier,
91
- ts,
90
+ ts=ts,
92
91
  )
93
92
  case OrbitRepresentation.OMM:
94
93
  omm = cast(OMM, satellite.orbit.data)
@@ -96,26 +95,26 @@ def _skyfield_sat_from_satellite(satellite: Satellite) -> EarthSatellite:
96
95
  case _:
97
96
  raise ValueError("Unknown orbit representation")
98
97
 
99
- _last_sat_identifier = satellite.identifier
98
+ _last_sat_cache_key = satellite.cache_key
100
99
  _last_sat = skyfield_sat
101
100
  return skyfield_sat
102
101
 
103
102
 
104
103
  _last_topos: Topos | None = None
105
- _last_identifier: str | None = None
104
+ _last_observer_cache_key: str | None = None
106
105
 
107
106
 
108
107
  def clear_observer_cache() -> None:
109
- global _last_topos, _last_identifier
108
+ global _last_topos, _last_observer_cache_key
110
109
 
111
110
  _last_topos = None
112
- _last_identifier = None
111
+ _last_observer_cache_key = None
113
112
 
114
113
 
115
114
  def _skyfield_topos_from_observer(observer: Observer) -> Topos:
116
- global _last_topos, _last_identifier
115
+ global _last_topos, _last_observer_cache_key
117
116
 
118
- if _last_identifier == observer.identifier and _last_topos is not None:
117
+ if _last_observer_cache_key == observer.cache_key and _last_topos is not None:
119
118
  return _last_topos
120
119
 
121
120
  _last_topos = Topos(
@@ -123,7 +122,7 @@ def _skyfield_topos_from_observer(observer: Observer) -> Topos:
123
122
  longitude_degrees=observer.lon_deg,
124
123
  elevation_m=observer.elevation_meters,
125
124
  )
126
- _last_identifier = observer.identifier
125
+ _last_observer_cache_key = observer.cache_key
127
126
  return _last_topos
128
127
 
129
128
 
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from hashlib import sha256
4
+
5
+
6
+ def cache_digest(*parts: str) -> str:
7
+ return sha256("".join(parts).encode()).hexdigest()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.6
3
+ Version: 1.0
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
@@ -46,6 +46,8 @@ from satnogs_predict import find_observation_windows
46
46
 
47
47
  For more complete examples and usage patterns, see [docs/docs.md](docs/docs.md).
48
48
 
49
+ For release notes and breaking changes, see [CHANGELOG.md](CHANGELOG.md).
50
+
49
51
  ## License
50
52
 
51
53
  [![license](https://img.shields.io/badge/license-AGPL%203.0-6672D8.svg)](LICENSE)
@@ -2,6 +2,7 @@
2
2
  .gitlab-ci.yml
3
3
  .pre-commit-config.yaml
4
4
  .python-version
5
+ CHANGELOG.md
5
6
  CONTRIBUTING.md
6
7
  LICENSE
7
8
  README.md
@@ -12,10 +13,13 @@ docs/docs.md
12
13
  src/satnogs_predict/__init__.py
13
14
  src/satnogs_predict/py.typed
14
15
  src/satnogs_predict/tle.py
16
+ src/satnogs_predict/utils.py
15
17
  src/satnogs_predict.egg-info/PKG-INFO
16
18
  src/satnogs_predict.egg-info/SOURCES.txt
17
19
  src/satnogs_predict.egg-info/dependency_links.txt
18
20
  src/satnogs_predict.egg-info/requires.txt
21
+ src/satnogs_predict.egg-info/scm_file_list.json
22
+ src/satnogs_predict.egg-info/scm_version.json
19
23
  src/satnogs_predict.egg-info/top_level.txt
20
24
  src/satnogs_predict/constraints/__init__.py
21
25
  src/satnogs_predict/constraints/constraints.py
@@ -0,0 +1,52 @@
1
+ {
2
+ "files": [
3
+ ".gitignore",
4
+ ".gitlab-ci.yml",
5
+ ".pre-commit-config.yaml",
6
+ "CHANGELOG.md",
7
+ ".python-version",
8
+ "README.md",
9
+ "CONTRIBUTING.md",
10
+ "uv.lock",
11
+ "pyproject.toml",
12
+ "LICENSE",
13
+ "setup.py",
14
+ "tests/conftest.py",
15
+ "tests/__init__.py",
16
+ "tests/helpers.py",
17
+ "tests/test_tle.py",
18
+ "tests/integration/test_fake_tle_integration.py",
19
+ "tests/domain/test_time.py",
20
+ "tests/domain/test_planner_domain.py",
21
+ "tests/domain/test_required_fields.py",
22
+ "tests/domain/test_orbit.py",
23
+ "tests/domain/test_observer.py",
24
+ "tests/domain/test_window.py",
25
+ "tests/planning/test_planner.py",
26
+ "tests/propagation/test_propagator.py",
27
+ "tests/core/test_engine.py",
28
+ "tests/constraints/test_constraints.py",
29
+ "src/satnogs_predict/__init__.py",
30
+ "src/satnogs_predict/py.typed",
31
+ "src/satnogs_predict/utils.py",
32
+ "src/satnogs_predict/tle.py",
33
+ "src/satnogs_predict/domain/constraints.py",
34
+ "src/satnogs_predict/domain/time.py",
35
+ "src/satnogs_predict/domain/observer.py",
36
+ "src/satnogs_predict/domain/validation.py",
37
+ "src/satnogs_predict/domain/planner.py",
38
+ "src/satnogs_predict/domain/__init__.py",
39
+ "src/satnogs_predict/domain/window.py",
40
+ "src/satnogs_predict/domain/geometry.py",
41
+ "src/satnogs_predict/domain/orbit.py",
42
+ "src/satnogs_predict/planning/planner.py",
43
+ "src/satnogs_predict/planning/__init__.py",
44
+ "src/satnogs_predict/propagation/__init__.py",
45
+ "src/satnogs_predict/propagation/propagator.py",
46
+ "src/satnogs_predict/core/engine.py",
47
+ "src/satnogs_predict/core/__init__.py",
48
+ "src/satnogs_predict/constraints/constraints.py",
49
+ "src/satnogs_predict/constraints/__init__.py",
50
+ "docs/docs.md"
51
+ ]
52
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0",
3
+ "distance": 0,
4
+ "node": "g302d7309b6fa2c41ed4296c82e03362c8a8350b7",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-01"
8
+ }
@@ -0,0 +1,59 @@
1
+ from satnogs_predict.domain.observer import Observer
2
+
3
+
4
+ class TestObserver:
5
+ def test_given_topos_values_change_when_getting_cache_key_then_key_changes(self) -> None:
6
+ observer = Observer(
7
+ lat_deg=0.0,
8
+ lon_deg=0.0,
9
+ elevation_meters=0,
10
+ horizon_deg=0.0,
11
+ )
12
+
13
+ assert (
14
+ observer.cache_key
15
+ != Observer(
16
+ lat_deg=1.0,
17
+ lon_deg=0.0,
18
+ elevation_meters=0,
19
+ horizon_deg=0.0,
20
+ ).cache_key
21
+ )
22
+ assert (
23
+ observer.cache_key
24
+ != Observer(
25
+ lat_deg=0.0,
26
+ lon_deg=1.0,
27
+ elevation_meters=0,
28
+ horizon_deg=0.0,
29
+ ).cache_key
30
+ )
31
+ assert (
32
+ observer.cache_key
33
+ != Observer(
34
+ lat_deg=0.0,
35
+ lon_deg=0.0,
36
+ elevation_meters=1,
37
+ horizon_deg=0.0,
38
+ ).cache_key
39
+ )
40
+
41
+ def test_given_non_topos_values_changes_when_getting_cache_key_then_key_is_unchanged(
42
+ self,
43
+ ) -> None:
44
+ first = Observer(
45
+ lat_deg=0.0,
46
+ lon_deg=0.0,
47
+ elevation_meters=0,
48
+ horizon_deg=0.0,
49
+ identifier="OBS-1",
50
+ )
51
+ second = Observer(
52
+ lat_deg=0.0,
53
+ lon_deg=0.0,
54
+ elevation_meters=0,
55
+ horizon_deg=10.0,
56
+ identifier="OBS-2",
57
+ )
58
+
59
+ assert first.cache_key == second.cache_key
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from satnogs_predict.domain.orbit import OMM, TLE, Orbit, Satellite
6
+
7
+ OMM_FIELDS = {
8
+ "OBJECT_NAME": "ISS (ZARYA)",
9
+ "NORAD_CAT_ID": "25544",
10
+ "OBJECT_ID": "1998-067A",
11
+ "CLASSIFICATION_TYPE": "U",
12
+ "EPHEMERIS_TYPE": "0",
13
+ "ELEMENT_SET_NO": "999",
14
+ "REV_AT_EPOCH": "54773",
15
+ "EPOCH": "2026-01-13T05:00:55.753",
16
+ "MEAN_MOTION": "15.49269250",
17
+ "ECCENTRICITY": "0.0007741",
18
+ "INCLINATION": "51.6330",
19
+ "RA_OF_ASC_NODE": "346.6801",
20
+ "ARG_OF_PERICENTER": "12.6584",
21
+ "MEAN_ANOMALY": "347.4598",
22
+ "MEAN_MOTION_DOT": ".00008852",
23
+ "MEAN_MOTION_DDOT": "0",
24
+ "BSTAR": ".00016699",
25
+ }
26
+
27
+
28
+ class TestTle:
29
+ def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
30
+ tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
31
+
32
+ assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
33
+
34
+ def test_given_line1_or_line2_changes_when_getting_cache_key_then_key_changes(self) -> None:
35
+ tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
36
+
37
+ assert tle.cache_key != TLE(line0="LINE0", line1="CHANGED", line2="LINE2").cache_key
38
+ assert tle.cache_key != TLE(line0="LINE0", line1="LINE1", line2="CHANGED").cache_key
39
+
40
+ def test_given_only_line0_changes_when_getting_cache_key_then_key_is_unchanged(self) -> None:
41
+ tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
42
+
43
+ assert tle.cache_key == TLE(line0="CHANGED", line1="LINE1", line2="LINE2").cache_key
44
+
45
+
46
+ class TestOmm:
47
+ def test_given_fields_when_created_then_copies_fields(self) -> None:
48
+ fields = dict(OMM_FIELDS)
49
+
50
+ omm = OMM(fields=fields)
51
+ fields["OBJECT_NAME"] = "CHANGED"
52
+
53
+ assert omm.fields == OMM_FIELDS
54
+
55
+ def test_given_omm_when_mutating_fields_then_raises(self) -> None:
56
+ omm = OMM(fields=OMM_FIELDS)
57
+
58
+ with pytest.raises(TypeError):
59
+ omm.fields["OBJECT_NAME"] = "CHANGED"
60
+
61
+ def test_given_omm_when_to_dict_then_returns_copy_of_fields(self) -> None:
62
+ omm = OMM(fields=OMM_FIELDS)
63
+
64
+ result = omm.to_dict()
65
+ result["OBJECT_NAME"] = "CHANGED"
66
+
67
+ assert result != omm.fields
68
+ assert omm.fields == OMM_FIELDS
69
+
70
+ def test_given_same_fields_in_different_order_when_getting_cache_key_then_key_is_stable(
71
+ self,
72
+ ) -> None:
73
+ fields = {
74
+ "OBJECT_NAME": "ISS (ZARYA)",
75
+ "NORAD_CAT_ID": "25544",
76
+ }
77
+ reversed_fields = {
78
+ "NORAD_CAT_ID": "25544",
79
+ "OBJECT_NAME": "ISS (ZARYA)",
80
+ }
81
+
82
+ assert OMM(fields=fields).cache_key == OMM(fields=reversed_fields).cache_key
83
+
84
+ def test_given_field_value_changes_when_getting_cache_key_then_key_changes(self) -> None:
85
+ fields = {
86
+ "OBJECT_NAME": "ISS (ZARYA)",
87
+ "NORAD_CAT_ID": "25544",
88
+ }
89
+ changed_fields = {
90
+ "OBJECT_NAME": "CHANGED",
91
+ "NORAD_CAT_ID": "25544",
92
+ }
93
+
94
+ assert OMM(fields=fields).cache_key != OMM(fields=changed_fields).cache_key
95
+
96
+
97
+ class TestOrbitFromOmm:
98
+ def test_given_omm_orbit_when_getting_cache_key_then_delegates_to_omm_data(self) -> None:
99
+ orbit = Orbit.from_omm(OMM_FIELDS)
100
+
101
+ assert orbit.cache_key == orbit.data.cache_key
102
+
103
+
104
+ class TestSatellite:
105
+ def test_given_only_identifier_changes_when_getting_cache_key_then_key_is_unchanged(
106
+ self,
107
+ ) -> None:
108
+ first = Satellite.from_tle(
109
+ line0="LINE0",
110
+ line1="LINE1",
111
+ line2="LINE2",
112
+ identifier="SAT-1",
113
+ )
114
+ second = Satellite.from_tle(
115
+ line0="LINE0",
116
+ line1="LINE1",
117
+ line2="LINE2",
118
+ identifier="SAT-2",
119
+ )
120
+
121
+ assert first.cache_key == second.cache_key
122
+
123
+ def test_given_tle_satellite_when_getting_cache_key_then_delegates_to_orbit(self) -> None:
124
+ sat = Satellite.from_tle(line0="LINE0", line1="LINE1", line2="LINE2")
125
+
126
+ assert sat.cache_key == sat.orbit.cache_key
127
+
128
+ def test_given_omm_satellite_when_getting_cache_key_then_delegates_to_orbit(self) -> None:
129
+ sat = Satellite.from_omm(fields=OMM_FIELDS)
130
+
131
+ assert sat.cache_key == sat.orbit.cache_key
@@ -8,7 +8,6 @@ from satnogs_predict.domain.geometry import GeometrySample
8
8
  from satnogs_predict.domain.orbit import OMM, TLE, Orbit, Satellite
9
9
  from satnogs_predict.domain.planner import PlanningConfig
10
10
  from satnogs_predict.domain.time import Duration, Instant, TimeRange
11
- from satnogs_predict.domain.window import ObservationWindow
12
11
 
13
12
 
14
13
  def _sample(micros: int) -> GeometrySample:
@@ -74,16 +73,6 @@ class TestRequiredFields:
74
73
  ),
75
74
  r"PlanningConfig\.overlap_handling cannot be None",
76
75
  ),
77
- (
78
- lambda: ObservationWindow(
79
- sat_identifier=None, # type: ignore[arg-type]
80
- observer_identifier="OBS",
81
- start_sample=_sample(0),
82
- end_sample=_sample(1_000_000),
83
- max_altitude_sample=_sample(500_000),
84
- ),
85
- r"ObservationWindow\.sat_identifier cannot be None",
86
- ),
87
76
  ],
88
77
  )
89
78
  def test_given_none_for_required_field_when_created_then_raises(
@@ -57,22 +57,35 @@ def window_seconds(
57
57
  )
58
58
 
59
59
 
60
- def make_observer(identifier: str | None = None) -> Observer:
60
+ def make_observer(
61
+ identifier: str = "",
62
+ *,
63
+ lat_deg: float = 0.0,
64
+ lon_deg: float = 0.0,
65
+ elevation_meters: int = 0,
66
+ horizon_deg: float = 0.0,
67
+ ) -> Observer:
61
68
  return Observer(
62
- lat_deg=0.0,
63
- lon_deg=0.0,
64
- elevation_meters=0,
65
- horizon_deg=0.0,
66
- identifier=identifier or next_observer_identifier(),
69
+ lat_deg=lat_deg,
70
+ lon_deg=lon_deg,
71
+ elevation_meters=elevation_meters,
72
+ horizon_deg=horizon_deg,
73
+ identifier=identifier,
67
74
  )
68
75
 
69
76
 
70
- def make_satellite(identifier: str | None = None) -> Satellite:
77
+ def make_satellite(
78
+ identifier: str = "",
79
+ *,
80
+ line0: str = "LINE0",
81
+ line1: str = "LINE1",
82
+ line2: str = "LINE2",
83
+ ) -> Satellite:
71
84
  return Satellite.from_tle(
72
- line1="LINE1",
73
- line2="LINE2",
74
- line0="LINE0",
75
- identifier=identifier or next_satellite_identifier(),
85
+ line0=line0,
86
+ line1=line1,
87
+ line2=line2,
88
+ identifier=identifier,
76
89
  )
77
90
 
78
91
 
@@ -1082,9 +1082,7 @@ class TestCalculateMinAltSkyfieldSample:
1082
1082
 
1083
1083
 
1084
1084
  class TestSkyfieldSatFromSatellite:
1085
- def test_given_same_identifier_when_called_twice_then_reuses_instance(
1086
- self, monkeypatch
1087
- ) -> None:
1085
+ def test_given_same_values_when_called_twice_then_reuses_instance(self, monkeypatch) -> None:
1088
1086
  created: list[object] = []
1089
1087
 
1090
1088
  class EarthSatelliteSpy:
@@ -1102,7 +1100,7 @@ class TestSkyfieldSatFromSatellite:
1102
1100
  assert first is second
1103
1101
  assert len(created) == 1
1104
1102
 
1105
- def test_given_different_identifier_when_called_then_returns_different_instances(
1103
+ def test_given_different_values_when_called_then_returns_different_instances(
1106
1104
  self, monkeypatch
1107
1105
  ) -> None:
1108
1106
  created: list[object] = []
@@ -1115,7 +1113,7 @@ class TestSkyfieldSatFromSatellite:
1115
1113
  propagator.clear_satellite_cache()
1116
1114
 
1117
1115
  first = propagator._skyfield_sat_from_satellite(make_satellite())
1118
- second = propagator._skyfield_sat_from_satellite(make_satellite())
1116
+ second = propagator._skyfield_sat_from_satellite(make_satellite(line1="LINE1CHANGED"))
1119
1117
 
1120
1118
  assert first is not second
1121
1119
  assert len(created) == 2
@@ -1164,9 +1162,7 @@ class TestSkyfieldSatFromSatellite:
1164
1162
 
1165
1163
 
1166
1164
  class TestSkyfieldToposFromObserver:
1167
- def test_given_same_identifier_when_called_twice_then_reuses_instance(
1168
- self, monkeypatch
1169
- ) -> None:
1165
+ def test_given_same_values_when_called_twice_then_reuses_instance(self, monkeypatch) -> None:
1170
1166
  created: list[object] = []
1171
1167
 
1172
1168
  class ToposSpy:
@@ -1184,7 +1180,7 @@ class TestSkyfieldToposFromObserver:
1184
1180
  assert first is second
1185
1181
  assert len(created) == 1
1186
1182
 
1187
- def test_given_different_identifier_when_called_then_returns_different_instances(
1183
+ def test_given_different_latlngalt_values_when_called_then_returns_different_instances(
1188
1184
  self, monkeypatch
1189
1185
  ) -> None:
1190
1186
  created: list[object] = []
@@ -1197,7 +1193,7 @@ class TestSkyfieldToposFromObserver:
1197
1193
  propagator.clear_observer_cache()
1198
1194
 
1199
1195
  first = propagator._skyfield_topos_from_observer(make_observer())
1200
- second = propagator._skyfield_topos_from_observer(make_observer())
1196
+ second = propagator._skyfield_topos_from_observer(make_observer(lat_deg=1.0))
1201
1197
 
1202
1198
  assert first is not second
1203
1199
  assert len(created) == 2
@@ -1,22 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from uuid import uuid4
5
-
6
- from .validation import ensure_required_fields_not_none
7
-
8
-
9
- @dataclass(frozen=True, slots=True)
10
- class Observer:
11
- """Observer to calculate passes over e.g. a ground station"""
12
-
13
- lat_deg: float
14
- lon_deg: float
15
- elevation_meters: int
16
- horizon_deg: float
17
- identifier: str = field(default_factory=lambda: str(uuid4()))
18
-
19
- def __post_init__(self) -> None:
20
- if self.identifier is None:
21
- object.__setattr__(self, "identifier", str(uuid4()))
22
- ensure_required_fields_not_none(self)
@@ -1,44 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from uuid import UUID
4
-
5
- from satnogs_predict.domain.observer import Observer
6
-
7
-
8
- class TestObserver:
9
- def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
10
- observer = Observer(
11
- lat_deg=0.0,
12
- lon_deg=0.0,
13
- elevation_meters=0,
14
- horizon_deg=0.0,
15
- )
16
-
17
- assert UUID(observer.identifier)
18
-
19
- def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
20
- observer = Observer(
21
- lat_deg=0.0,
22
- lon_deg=0.0,
23
- elevation_meters=0,
24
- horizon_deg=0.0,
25
- identifier=None, # type: ignore[arg-type]
26
- )
27
-
28
- assert UUID(observer.identifier)
29
-
30
- def test_given_no_identifier_when_created_twice_then_generates_unique_identifiers(self) -> None:
31
- first = Observer(
32
- lat_deg=0.0,
33
- lon_deg=0.0,
34
- elevation_meters=0,
35
- horizon_deg=0.0,
36
- )
37
- second = Observer(
38
- lat_deg=0.0,
39
- lon_deg=0.0,
40
- elevation_meters=0,
41
- horizon_deg=0.0,
42
- )
43
-
44
- assert first.identifier != second.identifier
@@ -1,146 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from uuid import UUID
4
-
5
- from satnogs_predict.domain.orbit import OMM, TLE, Orbit, OrbitRepresentation, Satellite
6
- from tests.helpers import next_satellite_identifier
7
-
8
- OMM_FIELDS = {
9
- "OBJECT_NAME": "ISS (ZARYA)",
10
- "NORAD_CAT_ID": "25544",
11
- "OBJECT_ID": "1998-067A",
12
- "CLASSIFICATION_TYPE": "U",
13
- "EPHEMERIS_TYPE": "0",
14
- "ELEMENT_SET_NO": "999",
15
- "REV_AT_EPOCH": "54773",
16
- "EPOCH": "2026-01-13T05:00:55.753",
17
- "MEAN_MOTION": "15.49269250",
18
- "ECCENTRICITY": "0.0007741",
19
- "INCLINATION": "51.6330",
20
- "RA_OF_ASC_NODE": "346.6801",
21
- "ARG_OF_PERICENTER": "12.6584",
22
- "MEAN_ANOMALY": "347.4598",
23
- "MEAN_MOTION_DOT": ".00008852",
24
- "MEAN_MOTION_DDOT": "0",
25
- "BSTAR": ".00016699",
26
- }
27
-
28
-
29
- class TestTle:
30
- def test_given_tle_when_to_list_then_returns_three_lines_in_order(self) -> None:
31
- tle = TLE(line0="LINE0", line1="LINE1", line2="LINE2")
32
-
33
- assert tle.to_list() == ["LINE0", "LINE1", "LINE2"]
34
-
35
-
36
- class TestOmm:
37
- def test_given_omm_when_to_dict_then_returns_copy_of_fields(self) -> None:
38
- omm = OMM(fields=OMM_FIELDS)
39
-
40
- result = omm.to_dict()
41
- result["OBJECT_NAME"] = "CHANGED"
42
-
43
- assert result != omm.fields
44
- assert omm.fields == OMM_FIELDS
45
-
46
-
47
- class TestOrbitFromOmm:
48
- def test_given_fields_when_created_then_builds_omm_orbit(self) -> None:
49
- orbit = Orbit.from_omm(OMM_FIELDS)
50
-
51
- assert orbit.representation == OrbitRepresentation.OMM
52
- assert orbit.data == OMM(fields=OMM_FIELDS)
53
-
54
- def test_given_fields_when_created_then_copies_fields(self) -> None:
55
- fields = dict(OMM_FIELDS)
56
-
57
- orbit = Orbit.from_omm(fields)
58
- fields["OBJECT_NAME"] = "CHANGED"
59
-
60
- assert orbit.data == OMM(fields=OMM_FIELDS)
61
-
62
-
63
- class TestSatelliteFromTle:
64
- def test_given_identifier_when_created_then_builds_satellite(self) -> None:
65
- line0 = "ISS (ZARYA)"
66
- line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
67
- line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
68
- identifier = next_satellite_identifier()
69
-
70
- sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
71
-
72
- assert sat.identifier == identifier
73
- assert sat.orbit.representation == OrbitRepresentation.TLE
74
- assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
75
-
76
- def test_given_identifier_when_created_then_uses_custom_value(self) -> None:
77
- line0 = "ISS (ZARYA)"
78
- line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
79
- line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
80
- identifier = "ISS"
81
-
82
- sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=identifier)
83
-
84
- assert sat.identifier == identifier
85
- assert sat.orbit.representation == OrbitRepresentation.TLE
86
- assert sat.orbit.data == TLE(line0=line0, line1=line1, line2=line2)
87
-
88
- def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
89
- line0 = "ISS (ZARYA)"
90
- line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
91
- line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
92
-
93
- sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2)
94
-
95
- assert UUID(sat.identifier)
96
-
97
- def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
98
- line0 = "ISS (ZARYA)"
99
- line1 = "1 25544U 98067A 26013.20897862 .00008852 00000-0 16699-3 0 9991"
100
- line2 = "2 25544 51.6330 346.6801 0007741 12.6584 347.4598 15.49269250547732"
101
-
102
- sat = Satellite.from_tle(line0=line0, line1=line1, line2=line2, identifier=None)
103
-
104
- assert UUID(sat.identifier)
105
-
106
-
107
- class TestSatelliteFromOmm:
108
- def test_given_identifier_when_created_then_builds_satellite(self) -> None:
109
- identifier = next_satellite_identifier()
110
-
111
- sat = Satellite.from_omm(fields=OMM_FIELDS, identifier=identifier)
112
-
113
- assert sat.identifier == identifier
114
- assert sat.orbit.representation == OrbitRepresentation.OMM
115
- assert sat.orbit.data == OMM(fields=OMM_FIELDS)
116
-
117
- def test_given_fields_when_created_then_copies_fields(self) -> None:
118
- fields = dict(OMM_FIELDS)
119
-
120
- sat = Satellite.from_omm(fields=fields, identifier=next_satellite_identifier())
121
- fields["OBJECT_NAME"] = "CHANGED"
122
-
123
- assert sat.orbit.data == OMM(fields=OMM_FIELDS)
124
-
125
- def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
126
- sat = Satellite.from_omm(fields=OMM_FIELDS)
127
-
128
- assert UUID(sat.identifier)
129
-
130
- def test_given_none_identifier_when_created_then_generates_identifier(self) -> None:
131
- sat = Satellite.from_omm(fields=OMM_FIELDS, identifier=None)
132
-
133
- assert UUID(sat.identifier)
134
-
135
-
136
- class TestSatellite:
137
- def test_given_no_identifier_when_created_then_generates_identifier(self) -> None:
138
- sat = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
139
-
140
- assert UUID(sat.identifier)
141
-
142
- def test_given_no_identifier_when_created_twice_then_generates_unique_identifiers(self) -> None:
143
- first = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
144
- second = Satellite(orbit=Orbit.from_omm(OMM_FIELDS))
145
-
146
- assert first.identifier != second.identifier
File without changes
File without changes
File without changes
File without changes
File without changes