satelles 0.0.0__py3-none-any.whl

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.
satelles/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ # **************************************************************************************
2
+
3
+ # @package satelles
4
+ # @license MIT License Copyright (c) 2025 Michael J. Roberts
5
+
6
+ # **************************************************************************************
7
+
8
+ from .satellite import Satellite
9
+ from .tle import TLE
10
+
11
+ # **************************************************************************************
12
+
13
+ __version__ = "0.0.0"
14
+
15
+ # **************************************************************************************
16
+
17
+ __license__ = "MIT"
18
+
19
+ # **************************************************************************************
20
+
21
+ __all__: list[str] = [
22
+ "Satellite",
23
+ "TLE",
24
+ ]
25
+
26
+ # **************************************************************************************
satelles/py.typed ADDED
File without changes
satelles/satellite.py ADDED
@@ -0,0 +1,177 @@
1
+ # **************************************************************************************
2
+
3
+ # @package satelles
4
+ # @license MIT License Copyright (c) 2025 Michael J. Roberts
5
+
6
+ # **************************************************************************************
7
+
8
+ from typing import Annotated
9
+
10
+ from pydantic import BaseModel, Field, field_validator
11
+
12
+ # **************************************************************************************
13
+
14
+
15
+ class ID(BaseModel):
16
+ id: Annotated[
17
+ int,
18
+ Field(
19
+ ge=0,
20
+ description="The satellite catalog number, e.g., NORAD ID",
21
+ ),
22
+ ]
23
+
24
+ name: Annotated[
25
+ str,
26
+ Field(
27
+ description="The designated name of the satellite",
28
+ ),
29
+ ]
30
+
31
+ classification: Annotated[
32
+ str,
33
+ Field(
34
+ description="The classification of the satellite, e.g., 'U' for unclassified, 'C' for classified, 'S' for secret",
35
+ ),
36
+ ]
37
+
38
+ designator: Annotated[
39
+ str,
40
+ Field(
41
+ description="The international designator of the satellite",
42
+ ),
43
+ ]
44
+
45
+ year: Annotated[
46
+ int,
47
+ Field(
48
+ ge=1900,
49
+ le=2100,
50
+ description="The Epoch year of the TLE (full four-digit year)",
51
+ ),
52
+ ]
53
+
54
+ day: Annotated[
55
+ float,
56
+ Field(
57
+ ge=1,
58
+ le=367,
59
+ description="Epoch day of the year with fractional portion included, e.g., 123.456789",
60
+ ),
61
+ ]
62
+
63
+ jd: Annotated[
64
+ float,
65
+ Field(
66
+ description="The Julian date of the Epoch",
67
+ ),
68
+ ]
69
+
70
+ ephemeris: Annotated[
71
+ int,
72
+ Field(
73
+ description="Ephemeris type (always zero; only used in undistributed TLE data)",
74
+ ),
75
+ ]
76
+
77
+ set: Annotated[
78
+ int,
79
+ Field(
80
+ ge=0,
81
+ description="The element set number, incremented when a new TLE is generated for this object",
82
+ ),
83
+ ]
84
+
85
+ @field_validator("classification")
86
+ def validate_classification(cls, value: str) -> str:
87
+ mapping = {"U": "Unclassified", "C": "Classified", "S": "Secret"}
88
+ if value not in mapping.keys():
89
+ raise ValueError(f"Classification must be one of {list(mapping.keys())}")
90
+ return mapping[value]
91
+
92
+
93
+ # **************************************************************************************
94
+
95
+
96
+ class OrbitalElements(BaseModel):
97
+ drag: Annotated[
98
+ float,
99
+ Field(
100
+ description="The B*, the drag term, or radiation pressure coefficient (decimal point assumed)",
101
+ ),
102
+ ]
103
+
104
+ raan: Annotated[
105
+ float,
106
+ Field(
107
+ description="Right Ascension of the ascending node (in degrees)",
108
+ ),
109
+ ]
110
+
111
+ inclination: Annotated[
112
+ float,
113
+ Field(
114
+ description="The orbital inclination of the satellite (in degrees)",
115
+ ),
116
+ ]
117
+
118
+ eccentricity: Annotated[
119
+ float,
120
+ Field(
121
+ description="The orbital eccentricity of the satellite (dimensionless)",
122
+ ),
123
+ ]
124
+
125
+ argument_of_perigee: Annotated[
126
+ float,
127
+ Field(
128
+ description="The argument of perigee of the satellite (in degrees)",
129
+ ),
130
+ ]
131
+
132
+ mean_anomaly: Annotated[
133
+ float,
134
+ Field(
135
+ description="The mean anomaly of the satellite (in degrees)",
136
+ ),
137
+ ]
138
+
139
+ mean_motion: Annotated[
140
+ float,
141
+ Field(
142
+ gt=0,
143
+ description="The mean motion (revolutions per day) of the satellite",
144
+ ),
145
+ ]
146
+
147
+ first_derivative_of_mean_motion: Annotated[
148
+ float,
149
+ Field(
150
+ description="The first derivative of mean motion (decimal point assumed) of the satellite 'the ballistic coefficient'",
151
+ ),
152
+ ]
153
+
154
+ second_derivative_of_mean_motion: Annotated[
155
+ float,
156
+ Field(
157
+ description="Second derivative of mean motion (decimal point assumed) of the satellite",
158
+ ),
159
+ ]
160
+
161
+ number_of_revolutions: Annotated[
162
+ int,
163
+ Field(
164
+ ge=0,
165
+ description="The number of complete revolutions the satellite has made around the Earth at the Epoch time",
166
+ ),
167
+ ]
168
+
169
+
170
+ # **************************************************************************************
171
+
172
+
173
+ class Satellite(ID, OrbitalElements):
174
+ pass
175
+
176
+
177
+ # **************************************************************************************
satelles/tle.py ADDED
@@ -0,0 +1,559 @@
1
+ # **************************************************************************************
2
+
3
+ # @package satelles
4
+ # @license MIT License Copyright (c) 2025 Michael J. Roberts
5
+
6
+ # **************************************************************************************
7
+
8
+ import re
9
+ from datetime import datetime, timedelta, timezone
10
+ from typing import Any, Optional, Tuple
11
+
12
+ from .satellite import Satellite
13
+
14
+ # **************************************************************************************
15
+
16
+ line1_regex = re.compile(
17
+ r"^1\s+"
18
+ r"(?P<id>[0-9A-Z]{5})"
19
+ r"(?P<classification>[A-Z])\s+"
20
+ r"(?P<designator>\d{2}\d{3}[A-Z]{1,3})\s+"
21
+ r"(?P<year>\d{2})(?P<day>\d{3}\.\d{8})\s+"
22
+ r"(?P<first_derivative_of_mean_motion>[+-]?\.\d{8})\s+"
23
+ r"(?P<second_derivative_of_mean_motion>[+-]?\d{5}[+-]\d)\s+"
24
+ r"(?P<drag>[+-]?\d{5}[+-]\d)\s+"
25
+ r"(?P<ephemeris>\d)\s+"
26
+ r"(?P<set>\d+)$"
27
+ )
28
+
29
+ # **************************************************************************************
30
+
31
+ line2_regex = re.compile(
32
+ r"^2\s+"
33
+ r"(?P<id>[0-9A-Z]{5})\s+"
34
+ r"(?P<inclination>\d+\.\d+)\s+"
35
+ r"(?P<raan>\d+\.\d+)\s+"
36
+ r"(?P<eccentricity>\d{7})\s+"
37
+ r"(?P<argument_of_perigee>\d+\.\d+)\s+"
38
+ r"(?P<mean_anomaly>\d+\.\d+)\s+"
39
+ r"(?P<mean_motion>\d+\.\d+)\s+"
40
+ r"(?P<number_of_revolutions>\d+)$"
41
+ )
42
+
43
+ # **************************************************************************************
44
+
45
+
46
+ def parse_scientific_notation(value: str) -> float:
47
+ """
48
+ Parse a string in the form ±DDDDD±D, where the first part is the mantissa (with an
49
+ implicit decimal) and the second part is the exponent.
50
+
51
+ Args:
52
+ value: The value to parse.
53
+
54
+ Returns:
55
+ The value as a float.
56
+ """
57
+ pattern = re.compile(r"^([+-]?)(\d{5})([+-]\d)$")
58
+
59
+ match = pattern.match(value)
60
+
61
+ if not match:
62
+ return 0.0
63
+
64
+ sign = -1 if match.group(1) == "-" else 1
65
+
66
+ mantissa = int(match.group(2)) / 1e5
67
+
68
+ exponent = int(match.group(3))
69
+
70
+ return sign * mantissa * (10**exponent)
71
+
72
+
73
+ # **************************************************************************************
74
+
75
+
76
+ def parse_id_field(value: Any) -> Optional[int]:
77
+ """
78
+ Parse a string as an integer, with support for base-36 encoding.
79
+
80
+ Args:
81
+ value: The value to parse.
82
+
83
+ Returns:
84
+ The value as an integer, or None if the value is not a valid integer
85
+ """
86
+ if not isinstance(value, str):
87
+ return None
88
+ try:
89
+ if value and value[0].isalpha():
90
+ return int(value, 36)
91
+ else:
92
+ return int(value, 10)
93
+ except ValueError:
94
+ return None
95
+
96
+
97
+ # **************************************************************************************
98
+
99
+
100
+ def parse_classification_field(value: Any) -> Optional[str]:
101
+ """
102
+ Parse a classification field, which is a single character representing the
103
+ classification of the satellite.
104
+
105
+ Args:
106
+ value: The value to parse.
107
+
108
+ Returns:
109
+ The classification of the satellite as a string, or None if the value is not
110
+ valid, e.g., either one of "U", "C", or "S".
111
+ """
112
+ if not isinstance(value, str):
113
+ return None
114
+
115
+ v = value.strip().upper()
116
+
117
+ if len(v) != 1:
118
+ return None
119
+
120
+ if v not in {"U", "C", "S"}:
121
+ raise ValueError("Invalid classification field. Must be one of: U, C, S")
122
+
123
+ return v
124
+
125
+
126
+ # **************************************************************************************
127
+
128
+
129
+ def parse_designator_field(value: Any) -> Optional[str]:
130
+ """
131
+ Parse a string as a designator field.
132
+
133
+ Args:
134
+ value: The value to parse.
135
+
136
+ Returns:
137
+ The value as a string, or None if the value is not a valid designator.
138
+ """
139
+ if not isinstance(value, str):
140
+ return None
141
+ return value.strip()
142
+
143
+
144
+ # **************************************************************************************
145
+
146
+
147
+ def parse_epoch_field(value: Any) -> Optional[Tuple[int, float, float]]:
148
+ if not isinstance(value, str):
149
+ return None
150
+ try:
151
+ # The first two digits represent the year (with a cutoff at 57):
152
+ year = int(value[:2])
153
+ if year < 57:
154
+ year += 2000
155
+ else:
156
+ year += 1900
157
+ day = float(value[2:])
158
+ except ValueError:
159
+ return None
160
+
161
+ # Compute the date corresponding to the day of year (note: day 1 is January 1):
162
+ dt = datetime(year, 1, 1, tzinfo=timezone.utc) + timedelta(days=day - 1)
163
+
164
+ # Convert Unix timestamp to Julian date:
165
+ jd = dt.timestamp() / 86400 + 2440587.5
166
+
167
+ return (year, day, jd)
168
+
169
+
170
+ # **************************************************************************************
171
+
172
+
173
+ def parse_bstar_drag_term_field(value: Any) -> Optional[float]:
174
+ if not isinstance(value, str):
175
+ return None
176
+ return parse_scientific_notation(value)
177
+
178
+
179
+ # **************************************************************************************
180
+
181
+
182
+ def parse_ephemeris_type_field(value: Any) -> Optional[int]:
183
+ if not isinstance(value, str):
184
+ return None
185
+ try:
186
+ return int(value)
187
+ except ValueError:
188
+ return None
189
+
190
+
191
+ # **************************************************************************************
192
+
193
+
194
+ def parse_set_number_field(value: Any) -> Optional[int]:
195
+ if not isinstance(value, str):
196
+ return None
197
+ try:
198
+ return int(value)
199
+ except ValueError:
200
+ return None
201
+
202
+
203
+ # **************************************************************************************
204
+
205
+
206
+ def parse_raan_field(value: Any) -> Optional[float]:
207
+ if not isinstance(value, str):
208
+ return None
209
+ try:
210
+ return float(value)
211
+ except ValueError:
212
+ return None
213
+
214
+
215
+ # **************************************************************************************
216
+
217
+
218
+ def parse_inclination_field(value: Any) -> Optional[float]:
219
+ if not isinstance(value, str):
220
+ return None
221
+ try:
222
+ return float(value)
223
+ except ValueError:
224
+ return None
225
+
226
+
227
+ # **************************************************************************************
228
+
229
+
230
+ def parse_eccentricity_field(value: Any) -> Optional[float]:
231
+ if not isinstance(value, str):
232
+ return None
233
+ try:
234
+ # The eccentricity field has an implicit leading zero.
235
+ return float("0." + value.strip())
236
+ except ValueError:
237
+ return None
238
+
239
+
240
+ # **************************************************************************************
241
+
242
+
243
+ def parse_argument_of_perigee_field(value: Any) -> Optional[float]:
244
+ if not isinstance(value, str):
245
+ return None
246
+ try:
247
+ return float(value)
248
+ except ValueError:
249
+ return None
250
+
251
+
252
+ # **************************************************************************************
253
+
254
+
255
+ def parse_mean_anomaly_field(value: Any) -> Optional[float]:
256
+ if not isinstance(value, str):
257
+ return None
258
+ try:
259
+ return float(value)
260
+ except ValueError:
261
+ return None
262
+
263
+
264
+ # **************************************************************************************s
265
+
266
+
267
+ def parse_mean_motion_field(value: Any) -> Optional[float]:
268
+ if not isinstance(value, str):
269
+ return None
270
+ try:
271
+ return float(value)
272
+ except ValueError:
273
+ return None
274
+
275
+
276
+ # **************************************************************************************
277
+
278
+
279
+ def parse_first_derivative_of_mean_motion_field(value: Any) -> Optional[float]:
280
+ if not isinstance(value, str):
281
+ return None
282
+ try:
283
+ return float(value)
284
+ except ValueError:
285
+ return None
286
+
287
+
288
+ # **************************************************************************************
289
+
290
+
291
+ def parse_second_derivative_of_mean_motion_field(value: Any) -> Optional[float]:
292
+ if not isinstance(value, str):
293
+ return None
294
+ return parse_scientific_notation(value)
295
+
296
+
297
+ # **************************************************************************************
298
+
299
+
300
+ def parse_number_of_revolutions_field(value: Any) -> Optional[int]:
301
+ if not isinstance(value, str):
302
+ return None
303
+ try:
304
+ return int(value)
305
+ except ValueError:
306
+ return None
307
+
308
+
309
+ # **************************************************************************************
310
+
311
+
312
+ def parse_tle(tle: str) -> Satellite:
313
+ """
314
+ Parse a TLE string and return a Satellite instance if successful, otherwise raise a
315
+ ValueError.
316
+
317
+ The TLE string can be in one of two formats:
318
+ - Three-line set (name, line1, line2)
319
+ - Two-line set (line1, line2), in which case the satellite name is set to an empty string.
320
+
321
+ Args:
322
+ tle: The TLE string to parse.
323
+
324
+ Raises:
325
+ ValueError: If the TLE string is not in a valid format or if any of the fields are invalid.
326
+
327
+ Returns:
328
+ A Satellite instance if parsing is successful.
329
+ """
330
+ # Split the TLE into lines and remove any empty lines:
331
+ lines = [line.strip() for line in tle.splitlines() if line.strip()]
332
+
333
+ if len(lines) == 2:
334
+ name = ""
335
+ line1, line2 = lines
336
+ elif len(lines) >= 3:
337
+ name = lines[0]
338
+ line1, line2 = lines[1], lines[2]
339
+ else:
340
+ raise ValueError("Invalid TLE format")
341
+
342
+ # Match the TLE to the line 1 regular expression:
343
+ m1 = line1_regex.match(line1)
344
+
345
+ # Match the TLE to the line 2 regular expression:
346
+ m2 = line2_regex.match(line2)
347
+
348
+ if not m1 or not m2:
349
+ raise ValueError("Invalid TLE format")
350
+
351
+ id_field = m1.group("id")
352
+ classification_field = m1.group("classification")
353
+ designator_field = m1.group("designator")
354
+ year_field = m1.group("year")
355
+ day_field = m1.group("day")
356
+ fdmm_field = m1.group("first_derivative_of_mean_motion")
357
+ sdmm_field = m1.group("second_derivative_of_mean_motion")
358
+ drag_field = m1.group("drag")
359
+ ephemeris_field = m1.group("ephemeris")
360
+ set_field = m1.group("set")
361
+ inclination_field = m2.group("inclination")
362
+ raan_field = m2.group("raan")
363
+ eccentricity_field = m2.group("eccentricity")
364
+ argument_of_perigee_field = m2.group("argument_of_perigee")
365
+ mean_anomaly_field = m2.group("mean_anomaly")
366
+ mean_motion_field = m2.group("mean_motion")
367
+ number_of_revolutions_field = m2.group("number_of_revolutions")
368
+
369
+ id = parse_id_field(id_field)
370
+
371
+ if id is None:
372
+ raise ValueError("Invalid ID field")
373
+
374
+ classification = parse_classification_field(classification_field)
375
+
376
+ if classification is None:
377
+ raise ValueError("Invalid classification field")
378
+
379
+ designator = parse_designator_field(designator_field)
380
+
381
+ if designator is None:
382
+ raise ValueError("Invalid designator field")
383
+
384
+ epoch = parse_epoch_field(f"{year_field}{day_field}")
385
+ if epoch is None:
386
+ raise ValueError("Invalid epoch field")
387
+
388
+ year, day, jd = epoch
389
+
390
+ fdmm = parse_first_derivative_of_mean_motion_field(fdmm_field)
391
+ if fdmm is None:
392
+ raise ValueError("Invalid first derivative of mean motion field")
393
+
394
+ sdmm = parse_second_derivative_of_mean_motion_field(sdmm_field)
395
+ if sdmm is None:
396
+ raise ValueError("Invalid second derivative of mean motion field")
397
+
398
+ drag = parse_bstar_drag_term_field(drag_field)
399
+ if drag is None:
400
+ raise ValueError("Invalid b* drag field")
401
+
402
+ ephemeris = parse_ephemeris_type_field(ephemeris_field)
403
+ if ephemeris is None:
404
+ raise ValueError("Invalid ephemeris type field")
405
+
406
+ set_number = parse_set_number_field(set_field)
407
+ if set_number is None:
408
+ raise ValueError("Invalid set number field")
409
+
410
+ inclination = parse_inclination_field(inclination_field)
411
+ if inclination is None:
412
+ raise ValueError("Invalid inclination field")
413
+
414
+ raan = parse_raan_field(raan_field)
415
+ if raan is None:
416
+ raise ValueError("Invalid raan field")
417
+
418
+ eccentricity = parse_eccentricity_field(eccentricity_field)
419
+ if eccentricity is None:
420
+ raise ValueError("Invalid eccentricity field")
421
+
422
+ argument_of_perigee = parse_argument_of_perigee_field(argument_of_perigee_field)
423
+ if argument_of_perigee is None:
424
+ raise ValueError("Invalid argument of perigee field")
425
+
426
+ mean_anomaly = parse_mean_anomaly_field(mean_anomaly_field)
427
+ if mean_anomaly is None:
428
+ raise ValueError("Invalid mean anomaly field")
429
+
430
+ mean_motion = parse_mean_motion_field(mean_motion_field)
431
+ if mean_motion is None:
432
+ raise ValueError("Invalid mean motion field")
433
+
434
+ number_of_revolutions = parse_number_of_revolutions_field(
435
+ number_of_revolutions_field
436
+ )
437
+ if number_of_revolutions is None:
438
+ raise ValueError("Invalid number of revolutions field")
439
+
440
+ # Assemble the satellite using the parsed fields and return it:
441
+ return Satellite(
442
+ id=id,
443
+ name=name,
444
+ classification=classification,
445
+ designator=designator,
446
+ year=year,
447
+ day=day,
448
+ jd=jd,
449
+ ephemeris=ephemeris,
450
+ set=set_number,
451
+ drag=drag,
452
+ raan=raan,
453
+ inclination=inclination,
454
+ eccentricity=eccentricity,
455
+ argument_of_perigee=argument_of_perigee,
456
+ mean_anomaly=mean_anomaly,
457
+ mean_motion=mean_motion,
458
+ first_derivative_of_mean_motion=fdmm,
459
+ second_derivative_of_mean_motion=sdmm,
460
+ number_of_revolutions=number_of_revolutions,
461
+ )
462
+
463
+
464
+ # **************************************************************************************
465
+
466
+
467
+ class TLE:
468
+ _satellite: Satellite
469
+
470
+ def __init__(self, tle_string: str) -> None:
471
+ # Parse the TLE string and create a Satellite instance:
472
+ try:
473
+ satellite = parse_tle(tle_string)
474
+ except ValueError as e:
475
+ raise ValueError(f"Invalid TLE string: {e}")
476
+
477
+ self._satellite = satellite
478
+
479
+ def __repr__(self) -> str:
480
+ return f"<TLE(name={self._satellite.name!r}, id={self._satellite.id})>"
481
+
482
+ @property
483
+ def name(self) -> str:
484
+ return self._satellite.name
485
+
486
+ @property
487
+ def id(self) -> int:
488
+ return self._satellite.id
489
+
490
+ @property
491
+ def classification(self) -> str:
492
+ return self._satellite.classification
493
+
494
+ @property
495
+ def designator(self) -> str:
496
+ return self._satellite.designator
497
+
498
+ @property
499
+ def year(self) -> int:
500
+ return self._satellite.year
501
+
502
+ @property
503
+ def day(self) -> float:
504
+ return self._satellite.day
505
+
506
+ @property
507
+ def julian_date(self) -> float:
508
+ return self._satellite.jd
509
+
510
+ @property
511
+ def ephemeris(self) -> int:
512
+ return self._satellite.ephemeris
513
+
514
+ @property
515
+ def set(self) -> int:
516
+ return self._satellite.set
517
+
518
+ @property
519
+ def b_star_drag(self) -> float:
520
+ return self._satellite.drag
521
+
522
+ @property
523
+ def right_ascension_of_the_ascending_node(self) -> float:
524
+ return self._satellite.raan
525
+
526
+ @property
527
+ def inclination(self) -> float:
528
+ return self._satellite.inclination
529
+
530
+ @property
531
+ def eccentricity(self) -> float:
532
+ return self._satellite.eccentricity
533
+
534
+ @property
535
+ def argument_of_perigee(self) -> float:
536
+ return self._satellite.argument_of_perigee
537
+
538
+ @property
539
+ def mean_anomaly(self) -> float:
540
+ return self._satellite.mean_anomaly
541
+
542
+ @property
543
+ def mean_motion(self) -> float:
544
+ return self._satellite.mean_motion
545
+
546
+ @property
547
+ def first_derivative_of_mean_motion(self) -> float:
548
+ return self._satellite.first_derivative_of_mean_motion
549
+
550
+ @property
551
+ def second_derivative_of_mean_motion(self) -> float:
552
+ return self._satellite.second_derivative_of_mean_motion
553
+
554
+ @property
555
+ def number_of_revolutions(self) -> int:
556
+ return self._satellite.number_of_revolutions
557
+
558
+
559
+ # **************************************************************************************
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: satelles
3
+ Version: 0.0.0
4
+ Summary: Add your description here
5
+ Author-email: michealroberts <michael@observerly.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: pydantic>=2.10.6
9
+ Description-Content-Type: text/markdown
10
+
11
+ # satelles
12
+ A hypermodern python package starter template with astral uv ruff and mypy
@@ -0,0 +1,8 @@
1
+ satelles/__init__.py,sha256=uJBz6mxm3IQ8VZnZoOdKKWCA2UUKf3M8BTYlF_Uxb2M,788
2
+ satelles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ satelles/satellite.py,sha256=2NiOPDP23aEmAUHLDnG05rBqR2E4xXY5OXwU5pSxZhU,4446
4
+ satelles/tle.py,sha256=aOdcqRJC75CcSEHY3cpVaLGaorm7_HdfqKrlC9mJOPc,15629
5
+ satelles-0.0.0.dist-info/METADATA,sha256=af0UZJT3-OKTzrNghN69SG2g_MT9ghVm0sqgaUxm2ws,346
6
+ satelles-0.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ satelles-0.0.0.dist-info/licenses/LICENSE,sha256=jHblKj3g6JEPKRCGlMqRHrCdZIyN_3NFu0aHrQht3FU,1075
8
+ satelles-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael J. Roberts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.