satnogs-predict 0.6__tar.gz → 1.1__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.1/CHANGELOG.md +19 -0
  2. {satnogs_predict-0.6 → satnogs_predict-1.1}/CONTRIBUTING.md +88 -10
  3. {satnogs_predict-0.6 → satnogs_predict-1.1}/PKG-INFO +3 -1
  4. {satnogs_predict-0.6 → satnogs_predict-1.1}/README.md +2 -0
  5. {satnogs_predict-0.6 → satnogs_predict-1.1}/docs/docs.md +21 -22
  6. satnogs_predict-1.1/src/satnogs_predict/domain/observer.py +35 -0
  7. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/orbit.py +37 -12
  8. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/window.py +3 -3
  9. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/propagation/propagator.py +13 -14
  10. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/tle.py +3 -3
  11. satnogs_predict-1.1/src/satnogs_predict/utils.py +7 -0
  12. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict.egg-info/PKG-INFO +3 -1
  13. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict.egg-info/SOURCES.txt +4 -0
  14. satnogs_predict-1.1/src/satnogs_predict.egg-info/scm_file_list.json +52 -0
  15. satnogs_predict-1.1/src/satnogs_predict.egg-info/scm_version.json +8 -0
  16. satnogs_predict-1.1/tests/domain/test_observer.py +59 -0
  17. satnogs_predict-1.1/tests/domain/test_orbit.py +131 -0
  18. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/domain/test_required_fields.py +0 -11
  19. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/helpers.py +24 -11
  20. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/integration/test_fake_tle_integration.py +7 -4
  21. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/propagation/test_propagator.py +6 -10
  22. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/test_tle.py +20 -0
  23. satnogs_predict-0.6/src/satnogs_predict/domain/observer.py +0 -22
  24. satnogs_predict-0.6/tests/domain/test_observer.py +0 -44
  25. satnogs_predict-0.6/tests/domain/test_orbit.py +0 -146
  26. {satnogs_predict-0.6 → satnogs_predict-1.1}/.gitignore +0 -0
  27. {satnogs_predict-0.6 → satnogs_predict-1.1}/.gitlab-ci.yml +0 -0
  28. {satnogs_predict-0.6 → satnogs_predict-1.1}/.pre-commit-config.yaml +0 -0
  29. {satnogs_predict-0.6 → satnogs_predict-1.1}/.python-version +0 -0
  30. {satnogs_predict-0.6 → satnogs_predict-1.1}/LICENSE +0 -0
  31. {satnogs_predict-0.6 → satnogs_predict-1.1}/pyproject.toml +0 -0
  32. {satnogs_predict-0.6 → satnogs_predict-1.1}/setup.cfg +0 -0
  33. {satnogs_predict-0.6 → satnogs_predict-1.1}/setup.py +0 -0
  34. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/__init__.py +0 -0
  35. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/constraints/__init__.py +0 -0
  36. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/constraints/constraints.py +0 -0
  37. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/core/__init__.py +0 -0
  38. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/core/engine.py +0 -0
  39. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/__init__.py +0 -0
  40. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/constraints.py +0 -0
  41. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/geometry.py +0 -0
  42. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/planner.py +0 -0
  43. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/time.py +0 -0
  44. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/domain/validation.py +0 -0
  45. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/planning/__init__.py +0 -0
  46. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/planning/planner.py +0 -0
  47. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/propagation/__init__.py +0 -0
  48. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict/py.typed +0 -0
  49. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict.egg-info/dependency_links.txt +0 -0
  50. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict.egg-info/requires.txt +0 -0
  51. {satnogs_predict-0.6 → satnogs_predict-1.1}/src/satnogs_predict.egg-info/top_level.txt +0 -0
  52. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/__init__.py +0 -0
  53. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/conftest.py +0 -0
  54. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/constraints/test_constraints.py +0 -0
  55. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/core/test_engine.py +0 -0
  56. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/domain/test_planner_domain.py +0 -0
  57. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/domain/test_time.py +0 -0
  58. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/domain/test_window.py +0 -0
  59. {satnogs_predict-0.6 → satnogs_predict-1.1}/tests/planning/test_planner.py +0 -0
  60. {satnogs_predict-0.6 → satnogs_predict-1.1}/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.
@@ -11,6 +11,7 @@
11
11
  * [Respond to review comments](#respond-to-review-comments)
12
12
  * [Don’t get discouraged - or impatient](#dont-get-discouraged-or-impatient)
13
13
  * [Sign your work - the Developer’s Certificate of Origin](#sign-your-work-the-developers-certificate-of-origin)
14
+ * [Generative AI / LLM Policy](#generative-ai--llm-policy)
14
15
  * [References](#references)
15
16
 
16
17
  ## Introduction ##
@@ -22,7 +23,7 @@ It is based on Linux kernel patch submission guidelines.
22
23
 
23
24
  All Libre Space Foundation projects code is maintained under Git.
24
25
  Obtain a copy of the source code using `git clone`.
25
- Development usually takes place under `master` or `dev` branch.
26
+ Development usually takes place under `main`, `master` or `dev` branch.
26
27
 
27
28
  ## Separate the changes ##
28
29
 
@@ -39,16 +40,16 @@ The point to remember is that each commit should make an easily understood chang
39
40
  Each commit should be justifiable on its own merits.
40
41
 
41
42
  When creating a merge request from a series of commits, take special care to ensure that the software builds and runs properly after each commit in the series.
42
- Developers using `git bisect` to track down a problem can end up splitting your merge request at any point; they will not thank you if you introduce bugs in the middle.
43
+ Developers using `git bisect` to track down a problem can end up splitting your merge request at any point; not thanking you if you introduce bugs in the middle.
43
44
 
44
45
  If you cannot condense your merge request into a smaller set of commits, then only submit say 15 or so at a time and wait for review and integration.
45
46
 
46
- Never commit binary files.
47
+ Never commit binary files or statically generated code.
47
48
  Once they are part of Git repository history they cannot be removed.
48
49
 
49
50
  ## Quality check of the changes ##
50
51
 
51
- Continuous Integration will check your changes automatically once you create a merge request.
52
+ Continuous Integration checks your changes automatically once you create a merge request.
52
53
  Nevertheless, local quality check should also be done before submitting a merge request.
53
54
  The checks can include:
54
55
  * Static code analysis and coverage
@@ -78,10 +79,10 @@ See [Separate the changes](#separate-the-changes).
78
79
 
79
80
  Create commit or merge requests descriptions that are self-contained.
80
81
  This benefits both the maintainers and reviewers.
81
- Always use your real name and e-mail address for committing changes.
82
+ Always use a known identity and email address for committing changes.
82
83
 
83
84
  If the commit fixes a reported issue, refer to that bug entry by issue number.
84
- The issue will be closed automatically after merge, if it matches [this regular expression](https://docs.gitlab.com/ce/user/project/issues/automatic_issue_closing.html).
85
+ The issue closes automatically after merge, if it matches [this regular expression](https://docs.gitlab.com/ce/user/project/issues/automatic_issue_closing.html).
85
86
 
86
87
  ## Create a merge request ##
87
88
 
@@ -91,7 +92,7 @@ Merge requests must not contain other merge requests; re-base your commits if ne
91
92
 
92
93
  ## Respond to review comments ##
93
94
 
94
- Your merge request will almost certainly get comments from reviewers on ways in which it can be improved.
95
+ Your merge request almost certainly gets comments from reviewers on ways in which it can be improved.
95
96
  You must respond to those comments; ignoring reviewers is a good way to get ignored in return.
96
97
  Be sure to tell the reviewers what changes you are making and to thank them for their time.
97
98
  Code review is a tiring and time-consuming process, and reviewers sometimes get grumpy.
@@ -106,9 +107,9 @@ Wait for a minimum of one week before pinging reviewers - possibly longer during
106
107
 
107
108
  ## Sign your work - the Developer’s Certificate of Origin ##
108
109
 
109
- To improve tracking of who did what, especially with commits that can percolate to their final resting place in Libre Space Foundation projects code-base through several layers of maintainers, we’ve introduced a “sign-off” procedure on commits.
110
+ To improve tracking of who did what, especially with commits that can percolate to their final resting place in Libre Space Foundation projects code-base through several layers of maintainers, a “sign-off” procedure on commits is enforced.
110
111
 
111
- The sign-off is a simple line at the end of the explanation for the commit, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch.
112
+ The sign-off is a simple line at the end of the explanation for the commit, which certifies that you wrote it or otherwise have the right to pass it on as an open source patch.
112
113
  The rules are pretty simple:
113
114
 
114
115
  if you can certify the below:
@@ -138,7 +139,7 @@ then you just add a line saying:
138
139
 
139
140
  `Signed-off-by: Random J Developer <random@developer.example.org>`
140
141
 
141
- using your real name (sorry, no pseudonyms or anonymous contributions.)
142
+ using a known identity (sorry, no anonymous contributions.)
142
143
 
143
144
  If you are a maintainer, sometimes you need to slightly modify commits order to merge them.
144
145
  If you stick strictly to rule (c), you should ask the submitter to rediff, but this is a totally counter-productive waste of time and energy.
@@ -152,6 +153,83 @@ Signed-off-by: Random J Developer <random@developer.example.org>
152
153
  Signed-off-by: Lucky K Maintainer <lucky@maintainer.example.org>
153
154
  ```
154
155
 
156
+ ## Generative AI / LLM Policy ##
157
+
158
+ We appreciate that we can't realistically police how you author your merge requests, which includes whether you employ large-language model (LLM)-based development tools.
159
+ So, we don't.
160
+
161
+ However, due to both legal and human reasons, we have to establish boundaries.
162
+
163
+ ### Overview ###
164
+
165
+ - We take the responsibility for this project very seriously and we expect you to take your responsibility for your contributions seriously, too.
166
+ This used to be a given, but it changed now that a merge request is just one prompt away.
167
+ - Every contribution has to be backed by a human who unequivocally owns the copyright for all changes.
168
+ No LLM bots in `Co-authored-by:`s.
169
+ - Repeated slop contributions are unacceptable, and will be closed without review.
170
+ We reserve the right to exclude contributors who continue to submit such material.
171
+ - Absolutely no unsupervised agentic tools like OpenClaw.
172
+
173
+ By submitting a merge request, you certify that:
174
+
175
+ - You are the author of the contribution or have the legal right to submit it.
176
+ - You either hold the copyright to the changes or have explicit legal authorization to contribute them under this project's license.
177
+ - You understand the code.
178
+ - You accept full responsibility for it.
179
+
180
+ ### Legal ###
181
+
182
+ There is ongoing legal uncertainty regarding the copyright status of LLM-generated works and their provenance.
183
+ Because of this, allowing contributions by LLMs has unpredictable consequences for the copyright status of this project - even when leaving aside possible copyright violations due to plagiarism.
184
+
185
+ ### Human ###
186
+
187
+ As the makers of software that is used by many people worldwide and with a reputation for high-quality maintenance, we take our responsibility to our users very seriously.
188
+ No matter what LLM vendors or boosters on LinkedIn tell you, we have to manually review every change before merging, because it's our responsibility to keep the project stable.
189
+
190
+ Please understand that by opening low-quality merge requests you're not helping anyone.
191
+ Worse, you're [poisoning the open source ecosystem](https://lwn.net/Articles/1058266/) that was precarious even before the arrival of LLM tools.
192
+ Having to wade through plausible-looking-but-low-quality merge requests and trying to determine which ones are legit is extremely demoralizing and has already burned out many good maintainers.
193
+
194
+ In particular, note that [code reviews are a social process](https://blog.glyph.im/2026/03/what-is-code-review-for.html) and are *not* intended to catch bugs in PRs created by an LLM.
195
+ If you submit a merge request, we expect you to engage with reviewers and learn from the review, not
196
+ simply update the prompt that generated the PR and regenerate the code.
197
+
198
+ Put bluntly, we have no time or interest to become part of your vibe coding loop where you drop LLM slop at our door, we spend time and energy to review it, and you just feed it back into the LLM for another iteration.
199
+
200
+ This dynamic is especially pernicious because it poisons the well for mentoring new contributors which we are committed to.
201
+
202
+ ### Summary ###
203
+
204
+ In practice, this means:
205
+
206
+ - Merge requests that have an LLM product listed as co-author can't be merged and will be closed without further discussion.
207
+ We cannot risk the copyright status of this project.
208
+
209
+ If you used LLM tools during development, you may still submit - but you must remove any LLM co-author tags and take full ownership of every line.
210
+
211
+ - By submitting a merge request, you take full technical and legal responsibility for the contents of the merge request and promise that you hold the copyright for the changes submitted.
212
+
213
+ "An LLM wrote it" is *not* an acceptable response to questions or critique.
214
+ If you cannot explain and defend the changes you submit, do not submit them and open a high-quality bug report or feature request instead.
215
+
216
+ - Accounts that exercise bot-like behavior, like automated mass merge requests, will be permanently banned, whether they belong to a human or not.
217
+
218
+ - LLM-generated comments must be concise and accurate, and you must be prepared to stand by them.
219
+ Do not post summaries unless you are certain that they add value to the discussion.
220
+ Remember that all LLM output *looks* plausible.
221
+ When using these tools, it's your responsibility to ensure that the output is correct, and useful.
222
+
223
+ - Remember that LLM generated content is generally easier for you to produce, but *harder* for others to read, review or interpret.
224
+ Prioritising your time over that of others is contrary to the project's code of conduct.
225
+ Verbose, repetitive, or off topic comments may be marked as spam.
226
+
227
+ ### Thanks ###
228
+
229
+ This LLM policy was based on the policy of the `pip` and `attrs` project.
230
+ Many thanks to them for developing it.
231
+
155
232
  ## References ##
156
233
 
157
234
  * Linux kernel documentation - [Submitting patches: the essential guide to getting your code into the kernel](https://www.kernel.org/doc/html/latest/process/submitting-patches.html)
235
+ * pip AI policy - [Generative AI / LLM Policy](https://github.com/pypa/pip/blob/main/AI_POLICY.md)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: satnogs-predict
3
- Version: 0.6
3
+ Version: 1.1
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
 
@@ -71,17 +71,17 @@ def generate_fake_tle(
71
71
  right_ascension = (observer_location.lst_hours_at(skyfield_time) * 15.0) % 360.0
72
72
  mean_anomaly = latitude % 360.0
73
73
 
74
- epoch_year = epoch.year
74
+ epoch_year = epoch.year % 100
75
75
  epoch_day = _epoch_day_of_year_fraction(epoch)
76
76
 
77
77
  line0 = params.name.ljust(24)
78
78
 
79
79
  line1_no_checksum = (
80
- "1 {:5.5}{:.1} {:8.8} {:.2}{:012.8f} {:>10.10} {:>8.8} {:>8.8} 0 {:>4.4}".format(
80
+ "1 {:5.5}{:.1} {:8.8} {:02d}{:012.8f} {:>10.10} {:>8.8} {:>8.8} 0 {:>4.4}".format(
81
81
  f"{params.catalog_num:05d}",
82
82
  params.classifier,
83
83
  params.cospar_id,
84
- str(epoch_year),
84
+ epoch_year,
85
85
  epoch_day,
86
86
  params.mean_motion_first,
87
87
  params.mean_motion_second,
@@ -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.1
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