pyephemera 0.4.1__py3-none-win_amd64.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.
ephemera/__init__.py ADDED
@@ -0,0 +1,809 @@
1
+ """Ephemera: a pocket-sized, MIT-licensed ephemeris for astrology and astronomy.
2
+
3
+ import ephemera as eph
4
+
5
+ jd = eph.julian_date(2026, 1, 1.5) # 1 Jan 2026, 12:00 UT
6
+ moon = eph.position(eph.Body.MOON, jd) # degrees, AU, per day
7
+ cusps = eph.houses(jd, -0.1278, 51.5074, eph.HouseSystem.PLACIDUS)
8
+ lahiri = eph.ayanamsa(eph.Ayanamsa.LAHIRI, eph.ut_to_tt(jd, 2026))
9
+
10
+ The 1" planetary series with Chiron (1550-2650) is built in. More data
11
+ files come with the package (``load_package_data('stars')``) or from the
12
+ repository (``load_data(path)``).
13
+
14
+ Angles are degrees, distances AU, times Julian days (UT or TT as named);
15
+ calls that find nothing return None. The data loaded is shared by the
16
+ whole process: load files before computing from several threads.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import datetime as _dt
22
+ import math
23
+ import os
24
+ from ctypes import byref, c_double, c_int
25
+ from dataclasses import dataclass
26
+ from enum import IntEnum, IntFlag
27
+ from typing import List, NamedTuple, Optional, Union
28
+
29
+ from . import _native as _n
30
+ from ._native import lib as _lib
31
+
32
+ __version__ = "0.4.1"
33
+
34
+ DEG = 180.0 / math.pi
35
+ _RAD = math.pi / 180.0
36
+ MINOR_BASE = 1000
37
+ """Body id of the first loaded minor planet (``minor_body`` gives the id)."""
38
+
39
+
40
+ class Body(IntEnum):
41
+ MERCURY = 0
42
+ VENUS = 1
43
+ EARTH = 2
44
+ MARS = 3
45
+ JUPITER = 4
46
+ SATURN = 5
47
+ URANUS = 6
48
+ NEPTUNE = 7
49
+ PLUTO = 8
50
+ SUN = 9
51
+ MOON = 10
52
+ CERES = 11
53
+ PALLAS = 12
54
+ JUNO = 13
55
+ VESTA = 14
56
+ CHIRON = 15
57
+ MEAN_NODE = 16
58
+ TRUE_NODE = 17
59
+ MEAN_LILITH = 18
60
+ OSCU_LILITH = 19
61
+ # The Hamburg school's Uranian points (Witte, Sieggrün).
62
+ CUPIDO = 20
63
+ HADES = 21
64
+ ZEUS = 22
65
+ KRONOS = 23
66
+ APOLLON = 24
67
+ ADMETOS = 25
68
+ VULKANUS = 26
69
+ POSEIDON = 27
70
+
71
+
72
+ class Ayanamsa(IntEnum):
73
+ LAHIRI = 0
74
+ FAGAN_BRADLEY = 1
75
+ RAMAN = 2
76
+ KRISHNAMURTI = 3
77
+ YUKTESHWAR = 4
78
+ DE_LUCE = 5
79
+ USHA_SHASHI = 6
80
+ DJWHAL_KHUL = 7
81
+ JN_BHASIN = 8
82
+ BABYLONIAN_KUGLER1 = 9
83
+ BABYLONIAN_KUGLER2 = 10
84
+ BABYLONIAN_KUGLER3 = 11
85
+ BABYLONIAN_HUBER = 12
86
+ BABYLONIAN_ETA_PISCIUM = 13
87
+ BABYLONIAN_ALDEBARAN = 14
88
+ HIPPARCHOS = 15
89
+ SASSANIAN = 16
90
+ J2000 = 17
91
+ J1900 = 18
92
+ B1950 = 19
93
+ SURYASIDDHANTA = 20
94
+ SURYASIDDHANTA_MEAN_SUN = 21
95
+ ARYABHATA = 22
96
+ ARYABHATA_MEAN_SUN = 23
97
+ SS_REVATI = 24
98
+ SS_CITRA = 25
99
+ SKYDRAM = 26
100
+ ARYABHATA_522 = 27
101
+ BABYLONIAN_BRITTON = 28
102
+ GALACTIC_EQUATOR_FIORENZA = 29
103
+ VETTIUS_VALENS = 30
104
+ LAHIRI_1940 = 31
105
+ LAHIRI_VP285 = 32
106
+ KRISHNAMURTI_SENTHILATHIBAN = 33
107
+ LAHIRI_ICRC = 34
108
+ # Star-based (need the stars: load_package_data('stars')).
109
+ TRUE_CITRA = 35
110
+ TRUE_REVATI = 36
111
+ TRUE_PUSHYA = 37
112
+ TRUE_MULA = 38
113
+ GALACTIC_CENTER = 39
114
+ GALACTIC_CENTER_BRAND = 40
115
+ GALACTIC_CENTER_COCHRANE = 41
116
+
117
+
118
+ class HouseSystem(IntEnum):
119
+ WHOLE_SIGN = 0
120
+ EQUAL = 1
121
+ PORPHYRY = 2
122
+ PLACIDUS = 3
123
+ KOCH = 4
124
+ REGIOMONTANUS = 5
125
+ CAMPANUS = 6
126
+ ALCABITIUS = 7
127
+ TOPOCENTRIC = 8
128
+ VEHLOW = 9
129
+ EQUAL_MC = 10
130
+ EQUAL_ARIES = 11
131
+ MERIDIAN = 12
132
+ MORINUS = 13
133
+
134
+
135
+ class OrbitMethod(IntEnum):
136
+ MEAN = 0
137
+ """Secular mean elements (Simon et al. 1994 / Meeus ch. 31): Mercury..Neptune."""
138
+ OSCULATING = 1
139
+ """From the position and velocity: the planets but the Earth, Pluto, minor bodies."""
140
+
141
+
142
+ class EclipseType(IntFlag):
143
+ TOTAL = 1
144
+ ANNULAR = 2
145
+ HYBRID = 4
146
+ PARTIAL = 8
147
+ PENUMBRAL = 16
148
+ NON_CENTRAL = 32
149
+
150
+
151
+ # ---- results ---------------------------------------------------------------
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class Position:
156
+ longitude: float
157
+ latitude: float
158
+ distance: float
159
+ longitude_speed: float
160
+ latitude_speed: float
161
+ distance_speed: float
162
+
163
+ @property
164
+ def retrograde(self) -> bool:
165
+ return self.longitude_speed < 0
166
+
167
+
168
+ class CalendarDate(NamedTuple):
169
+ year: int
170
+ month: int
171
+ day: float # with the fraction of the day
172
+
173
+
174
+ class JulianDates(NamedTuple):
175
+ jd_tt: float
176
+ jd_ut: float
177
+
178
+
179
+ class Nutation(NamedTuple):
180
+ longitude: float
181
+ obliquity: float
182
+
183
+
184
+ class Equatorial(NamedTuple):
185
+ right_ascension: float
186
+ declination: float
187
+
188
+
189
+ class Heliocentric(NamedTuple):
190
+ longitude: float
191
+ latitude: float
192
+ distance: float
193
+
194
+
195
+ class AscMc(NamedTuple):
196
+ asc: float
197
+ mc: float
198
+
199
+
200
+ class HousesInfo(NamedTuple):
201
+ cusps: List[float]
202
+ fallback: bool # the system is undefined here (polar): Porphyry was used
203
+
204
+
205
+ @dataclass(frozen=True)
206
+ class HouseAngles:
207
+ ascendant: float
208
+ mc: float
209
+ armc: float
210
+ vertex: float
211
+ equatorial_ascendant: float
212
+ co_ascendant_koch: float
213
+ co_ascendant_munkasey: float
214
+ polar_ascendant: float
215
+
216
+
217
+ @dataclass(frozen=True)
218
+ class Phenomena:
219
+ phase_angle: float
220
+ phase: float # illuminated fraction, 0..1
221
+ elongation: float
222
+ diameter: float
223
+ magnitude: float
224
+
225
+
226
+ class CustomAyanamsa(NamedTuple):
227
+ """A user-defined ayanamsa: its value (degrees) at a TT Julian day."""
228
+ jd_t0: float
229
+ value_at_t0: float
230
+
231
+
232
+ class NodeCrossing(NamedTuple):
233
+ jd: float
234
+ node: str # 'ascending' or 'descending'
235
+ longitude: float
236
+
237
+
238
+ class Station(NamedTuple):
239
+ jd: float
240
+ turns: str # 'retrograde' or 'direct'
241
+
242
+
243
+ @dataclass(frozen=True)
244
+ class OrbitPoints:
245
+ ascending_node: Position
246
+ descending_node: Position
247
+ perihelion: Position
248
+ aphelion: Position
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class Orbit:
253
+ """A heliocentric orbit, mean ecliptic and equinox of date."""
254
+ semi_major_axis: float
255
+ eccentricity: float
256
+ inclination: float
257
+ node: float
258
+ perihelion: float
259
+ heliocentric: OrbitPoints
260
+ geocentric: OrbitPoints
261
+
262
+
263
+ @dataclass(frozen=True)
264
+ class SolarEclipse:
265
+ """Times are TT Julian days; 0 marks a phase that does not occur."""
266
+ type: EclipseType
267
+ maximum: float
268
+ gamma: float
269
+ magnitude: float
270
+ latitude: float
271
+ longitude: float
272
+ saros: int
273
+ partial_begin: float
274
+ partial_end: float
275
+ central_begin: float
276
+ central_end: float
277
+ center_line_begin: float
278
+ center_line_end: float
279
+
280
+
281
+ @dataclass(frozen=True)
282
+ class LocalSolarEclipse:
283
+ type: EclipseType
284
+ maximum: float
285
+ first_contact: float
286
+ second_contact: float
287
+ third_contact: float
288
+ fourth_contact: float
289
+ magnitude: float
290
+ obscuration: float
291
+ altitude: float # of the Sun at maximum; below 0: not visible
292
+ saros: int
293
+
294
+
295
+ @dataclass(frozen=True)
296
+ class LocalEclipseState:
297
+ type: EclipseType # 0: none
298
+ magnitude: float
299
+ obscuration: float
300
+ altitude: float
301
+
302
+
303
+ @dataclass(frozen=True)
304
+ class LunarEclipse:
305
+ type: EclipseType
306
+ maximum: float
307
+ gamma: float
308
+ umbral_magnitude: float
309
+ penumbral_magnitude: float
310
+ saros: int
311
+ penumbral_begin: float
312
+ penumbral_end: float
313
+ partial_begin: float
314
+ partial_end: float
315
+ total_begin: float
316
+ total_end: float
317
+
318
+
319
+ class GeoPoint(NamedTuple):
320
+ latitude: float
321
+ longitude: float
322
+
323
+
324
+ class MinorPlanet(NamedTuple):
325
+ body: int
326
+ number: int
327
+ name: str
328
+
329
+
330
+ class SplitLongitude(NamedTuple):
331
+ sign: int # 0 = Aries .. 11 = Pisces
332
+ degrees: int
333
+ minutes: int
334
+ seconds: float
335
+
336
+
337
+ # ---- helpers -----------------------------------------------------------------
338
+
339
+
340
+ def _year(jd: float) -> float:
341
+ return 2000.0 + (jd - 2451545.0) / 365.25
342
+
343
+
344
+ def _position(p: _n.Position) -> Position:
345
+ return Position(p.longitude * DEG, p.latitude * DEG, p.distance,
346
+ p.longitude_speed * DEG, p.latitude_speed * DEG, p.distance_speed)
347
+
348
+
349
+ def _observer(lon: float, lat: float, elevation: float = 0.0) -> _n.Observer:
350
+ return _n.Observer(lon * _RAD, lat * _RAD, elevation)
351
+
352
+
353
+ def _str(b: Optional[bytes]) -> str:
354
+ return b.decode("utf-8") if b else ""
355
+
356
+
357
+ # ---- data files -------------------------------------------------------------
358
+
359
+ PACKAGE_DATA = ("1as", "1as-chiron", "01as", "01as-chiron", "chiron", "asteroids", "stars")
360
+ """Data files shipped in the package (data/eph-<name>.bin)."""
361
+
362
+
363
+ def data_path(name: str) -> str:
364
+ """Path of a packaged data file, e.g. ``data_path('stars')``."""
365
+ if name not in PACKAGE_DATA:
366
+ raise ValueError(f"unknown data file {name!r}; packaged: {', '.join(PACKAGE_DATA)}")
367
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", f"eph-{name}.bin")
368
+
369
+
370
+ def load_data(source: Union[bytes, bytearray, memoryview, str, os.PathLike]) -> bool:
371
+ """Load a data file from its bytes or a path; False if it is rejected.
372
+ Later files override what earlier ones provide."""
373
+ if isinstance(source, (bytes, bytearray, memoryview)):
374
+ data = bytes(source)
375
+ return _lib.eph_load_data(data, len(data)) == 0 # the library copies it
376
+ return _lib.eph_load_file(os.fsencode(source)) == 0
377
+
378
+
379
+ def load_package_data(*names: str) -> None:
380
+ """Load packaged data files: '01as', 'chiron', 'asteroids', 'stars', ..."""
381
+ for name in names:
382
+ if not load_data(data_path(name)):
383
+ raise ValueError(f"cannot load data file {name!r}")
384
+
385
+
386
+ def unload_data() -> None:
387
+ """Unload every data file (the built-in data too; see load_embedded)."""
388
+ _lib.eph_unload_data()
389
+
390
+
391
+ def load_embedded() -> bool:
392
+ """Reload the built-in data (the 1" series with Chiron)."""
393
+ return _lib.eph_load_embedded() == 0
394
+
395
+
396
+ # ---- time --------------------------------------------------------------------
397
+
398
+
399
+ def julian_date(year: int, month: int, day: float) -> float:
400
+ """Julian day of a calendar date (Gregorian; Julian before 1582-10-15);
401
+ ``day`` carries the fraction of the day (1.5 = noon)."""
402
+ return _lib.eph_julian_date(year, month, day)
403
+
404
+
405
+ def calendar_date(jd: float) -> CalendarDate:
406
+ y, m, d = c_int(), c_int(), c_double()
407
+ _lib.eph_calendar_date(jd, byref(y), byref(m), byref(d))
408
+ return CalendarDate(y.value, m.value, d.value)
409
+
410
+
411
+ def delta_t(year: float) -> float:
412
+ """TT - UT, seconds."""
413
+ return _lib.eph_delta_t(year)
414
+
415
+
416
+ def ut_to_tt(jd_ut: float, year: Optional[float] = None) -> float:
417
+ return _lib.eph_ut_to_tt(jd_ut, _year(jd_ut) if year is None else year)
418
+
419
+
420
+ def tt_to_ut(jd_tt: float, year: Optional[float] = None) -> float:
421
+ return _lib.eph_tt_to_ut(jd_tt, _year(jd_tt) if year is None else year)
422
+
423
+
424
+ def utc_to_jd(year: Union[int, _dt.datetime], month: int = 1, day: int = 1, hour: int = 0,
425
+ minute: int = 0, second: float = 0.0) -> Optional[JulianDates]:
426
+ """Civil UTC (leap seconds from 1972) to TT and UT Julian days; None for an
427
+ invalid date. Also takes a datetime: an aware one is converted to UTC, a
428
+ naive one is taken as UTC."""
429
+ if isinstance(year, _dt.datetime):
430
+ t = year
431
+ if t.tzinfo is not None:
432
+ t = t.astimezone(_dt.timezone.utc)
433
+ year, month, day, hour, minute = t.year, t.month, t.day, t.hour, t.minute
434
+ second = t.second + t.microsecond / 1e6
435
+ tt, ut = c_double(), c_double()
436
+ if _lib.eph_utc_to_jd(year, month, day, hour, minute, second, byref(tt), byref(ut)) != 0:
437
+ return None
438
+ return JulianDates(tt.value, ut.value)
439
+
440
+
441
+ def tdb_minus_tt(jd_tt: float) -> float:
442
+ """TDB - TT, seconds."""
443
+ return _lib.eph_tdb_minus_tt(jd_tt)
444
+
445
+
446
+ def sidereal_time(jd_ut: float) -> float:
447
+ """Greenwich apparent sidereal time (IAU 2006), degrees."""
448
+ return _lib.eph_sidereal_time(jd_ut) * DEG
449
+
450
+
451
+ def equation_of_time(jd_ut: float) -> Optional[float]:
452
+ """Apparent minus mean solar time, minutes; None without the Sun's data."""
453
+ e = c_double()
454
+ return e.value * 1440.0 if _lib.eph_equation_of_time(jd_ut, byref(e)) == 0 else None
455
+
456
+
457
+ # ---- frames ----------------------------------------------------------------
458
+
459
+
460
+ def mean_obliquity(jd_tt: float) -> float:
461
+ """Mean obliquity of the ecliptic (IAU 2006), degrees."""
462
+ return _lib.eph_mean_obliquity(jd_tt) * DEG
463
+
464
+
465
+ def nutation(jd_tt: float) -> Nutation:
466
+ """Nutation in longitude and obliquity (IAU 2000B), degrees."""
467
+ dpsi, deps = c_double(), c_double()
468
+ _lib.eph_nutation(jd_tt, byref(dpsi), byref(deps))
469
+ return Nutation(dpsi.value * DEG, deps.value * DEG)
470
+
471
+
472
+ def ecliptic_to_equatorial(lon: float, lat: float, jd_tt: float) -> Equatorial:
473
+ """Apparent ecliptic to equatorial coordinates (true obliquity), degrees."""
474
+ ra, dec = c_double(), c_double()
475
+ _lib.eph_ecliptic_to_equatorial(lon * _RAD, lat * _RAD, jd_tt, byref(ra), byref(dec))
476
+ return Equatorial(ra.value * DEG, dec.value * DEG)
477
+
478
+
479
+ # ---- positions ---------------------------------------------------------------
480
+
481
+
482
+ def position(body: int, jd_ut: float, apparent: bool = True,
483
+ year: Optional[float] = None) -> Optional[Position]:
484
+ """Geocentric ecliptic position of date for a UT Julian day, apparent by
485
+ default; None without data. ``year`` (fractional) selects ΔT."""
486
+ out = _n.Position()
487
+ fn = _lib.eph_position_apparent_ut if apparent else _lib.eph_position_ut
488
+ rc = fn(body, jd_ut, _year(jd_ut) if year is None else year, byref(out))
489
+ return _position(out) if rc == 0 else None
490
+
491
+
492
+ def position_tt(body: int, jd_tt: float, apparent: bool = True) -> Optional[Position]:
493
+ out = _n.Position()
494
+ fn = _lib.eph_position_apparent_tt if apparent else _lib.eph_position_tt
495
+ return _position(out) if fn(body, jd_tt, byref(out)) == 0 else None
496
+
497
+
498
+ def heliocentric(body: int, jd_tt: float) -> Optional[Heliocentric]:
499
+ """Heliocentric ecliptic of date; None for the Sun, the Moon or without data."""
500
+ lon, lat, dist = c_double(), c_double(), c_double()
501
+ if _lib.eph_heliocentric_tt(body, jd_tt, byref(lon), byref(lat), byref(dist)) != 0:
502
+ return None
503
+ return Heliocentric(lon.value * DEG, lat.value * DEG, dist.value)
504
+
505
+
506
+ def body_name(body: int) -> str:
507
+ """'Sun', 'Moon', ... (minor planets: their names)."""
508
+ if body >= MINOR_BASE:
509
+ return minor_name(body)
510
+ return _str(_lib.eph_body_name(body))
511
+
512
+
513
+ def topocentric(body: int, jd_ut: float, lon: float, lat: float, elevation: float = 0.0,
514
+ year: Optional[float] = None) -> Optional[Position]:
515
+ """Apparent position seen from a place (east longitude, latitude in degrees,
516
+ elevation in metres)."""
517
+ out = _n.Position()
518
+ obs = _observer(lon, lat, elevation)
519
+ rc = _lib.eph_position_topocentric_ut(body, jd_ut, _year(jd_ut) if year is None else year,
520
+ byref(obs), byref(out))
521
+ return _position(out) if rc == 0 else None
522
+
523
+
524
+ # ---- houses ------------------------------------------------------------------
525
+
526
+
527
+ def asc_mc(jd_ut: float, lon: float, lat: float) -> AscMc:
528
+ asc, mc = c_double(), c_double()
529
+ obs = _observer(lon, lat)
530
+ _lib.eph_asc_mc(jd_ut, byref(obs), byref(asc), byref(mc))
531
+ return AscMc(asc.value * DEG, mc.value * DEG)
532
+
533
+
534
+ def houses_info(jd_ut: float, lon: float, lat: float,
535
+ system: int = HouseSystem.PORPHYRY) -> Optional[HousesInfo]:
536
+ """The twelve cusps, and whether the system is undefined at this latitude
537
+ (Placidus, Koch, Alcabitius inside the polar circles: Porphyry is used)."""
538
+ cusps = (c_double * 12)()
539
+ obs = _observer(lon, lat)
540
+ rc = _lib.eph_houses(jd_ut, byref(obs), system, cusps)
541
+ if rc < 0:
542
+ return None
543
+ return HousesInfo([c * DEG for c in cusps], rc == 1)
544
+
545
+
546
+ def houses(jd_ut: float, lon: float, lat: float,
547
+ system: int = HouseSystem.PORPHYRY) -> Optional[List[float]]:
548
+ """Twelve cusps (degrees); None on bad arguments."""
549
+ info = houses_info(jd_ut, lon, lat, system)
550
+ return info.cusps if info else None
551
+
552
+
553
+ def house_angles(jd_ut: float, lon: float, lat: float) -> HouseAngles:
554
+ """Ascendant, MC, ARMC, Vertex, equatorial Ascendant, co-Ascendants and
555
+ polar Ascendant, degrees."""
556
+ out = _n.Angles()
557
+ obs = _observer(lon, lat)
558
+ _lib.eph_house_angles(jd_ut, byref(obs), byref(out))
559
+ return HouseAngles(*(getattr(out, f) * DEG for f, _ in _n.Angles._fields_))
560
+
561
+
562
+ def house_position(cusps: List[float], lon: float) -> float:
563
+ """House 1.0 .. 12.999 of a longitude, given cusps from houses()."""
564
+ arr = (c_double * 12)(*(c * _RAD for c in cusps))
565
+ return _lib.eph_house_position(arr, lon * _RAD)
566
+
567
+
568
+ # ---- phenomena -----------------------------------------------------------------
569
+
570
+
571
+ def phenomena(body: int, jd_tt: float) -> Optional[Phenomena]:
572
+ """Phase angle, illuminated fraction, elongation, apparent diameter
573
+ (degrees) and visual magnitude; None without data."""
574
+ out = _n.Phenomena()
575
+ if _lib.eph_phenomena_tt(body, jd_tt, byref(out)) != 0:
576
+ return None
577
+ return Phenomena(out.phase_angle * DEG, out.phase, out.elongation * DEG,
578
+ out.diameter * DEG, out.magnitude)
579
+
580
+
581
+ # ---- searches --------------------------------------------------------------------
582
+
583
+ AyanamsaOption = Union[int, CustomAyanamsa]
584
+
585
+
586
+ def longitude_crossing(body: int, lon: float, jd_ut: float, direction: int = 1,
587
+ max_days: float = 400, ayanamsa: Optional[AyanamsaOption] = None
588
+ ) -> Optional[float]:
589
+ """When the apparent longitude reaches ``lon`` (returns, ingresses); UT in
590
+ and out, None if not within max_days. With ``ayanamsa`` (an Ayanamsa or a
591
+ CustomAyanamsa) the longitude is sidereal, the ayanamsa taken at the
592
+ crossing itself (sidereal returns and ingresses)."""
593
+ out = c_double()
594
+ if ayanamsa is None:
595
+ rc = _lib.eph_longitude_crossing_ut(body, lon * _RAD, jd_ut, direction, max_days, byref(out))
596
+ return out.value if rc == 0 else None
597
+ jd_tt = ut_to_tt(jd_ut)
598
+ if isinstance(ayanamsa, CustomAyanamsa):
599
+ rc = _lib.eph_longitude_crossing_sidereal_custom(
600
+ body, lon * _RAD, ayanamsa.jd_t0, ayanamsa.value_at_t0 * _RAD, jd_tt, direction,
601
+ max_days, byref(out))
602
+ else:
603
+ rc = _lib.eph_longitude_crossing_sidereal(body, lon * _RAD, int(ayanamsa), jd_tt, direction,
604
+ max_days, byref(out))
605
+ return tt_to_ut(out.value) if rc == 0 else None
606
+
607
+
608
+ def heliocentric_crossing(body: int, lon: float, jd_tt: float, direction: int = 1,
609
+ max_days: float = 40000, ayanamsa: Optional[AyanamsaOption] = None
610
+ ) -> Optional[float]:
611
+ """When the heliocentric longitude reaches ``lon``; TT in and out."""
612
+ out = c_double()
613
+ if ayanamsa is None:
614
+ rc = _lib.eph_heliocentric_crossing(body, lon * _RAD, jd_tt, direction, max_days, byref(out))
615
+ elif isinstance(ayanamsa, CustomAyanamsa):
616
+ rc = _lib.eph_heliocentric_crossing_sidereal_custom(
617
+ body, lon * _RAD, ayanamsa.jd_t0, ayanamsa.value_at_t0 * _RAD, jd_tt, direction,
618
+ max_days, byref(out))
619
+ else:
620
+ rc = _lib.eph_heliocentric_crossing_sidereal(body, lon * _RAD, int(ayanamsa), jd_tt,
621
+ direction, max_days, byref(out))
622
+ return out.value if rc == 0 else None
623
+
624
+
625
+ def node_crossing(body: int, jd_tt: float, direction: int = 1,
626
+ max_days: float = 40) -> Optional[NodeCrossing]:
627
+ """When the body crosses the ecliptic; TT in and out."""
628
+ jd, lon = c_double(), c_double()
629
+ rc = _lib.eph_node_crossing(body, jd_tt, direction, max_days, byref(jd), byref(lon))
630
+ if rc < 1:
631
+ return None
632
+ return NodeCrossing(jd.value, "ascending" if rc == 1 else "descending", lon.value * DEG)
633
+
634
+
635
+ def station(body: int, jd_tt: float, direction: int = 1,
636
+ max_days: float = 800) -> Optional[Station]:
637
+ """When the body turns retrograde or direct; TT in and out."""
638
+ jd = c_double()
639
+ rc = _lib.eph_station(body, jd_tt, direction, max_days, byref(jd))
640
+ if rc < 1:
641
+ return None
642
+ return Station(jd.value, "retrograde" if rc == 1 else "direct")
643
+
644
+
645
+ # ---- planetary nodes and apsides --------------------------------------------------
646
+
647
+
648
+ def orbit(body: int, jd_tt: float, method: int = OrbitMethod.MEAN) -> Optional[Orbit]:
649
+ """The orbit's elements (degrees, AU) and its nodes, perihelion and
650
+ aphelion, heliocentric and geocentric; None if unsupported or without data."""
651
+ out = _n.Orbit()
652
+ if _lib.eph_orbit_tt(body, jd_tt, method, byref(out)) != 0:
653
+ return None
654
+
655
+ def points(arr) -> OrbitPoints:
656
+ return OrbitPoints(*(_position(arr[i]) for i in range(4)))
657
+
658
+ return Orbit(out.semi_major_axis, out.eccentricity, out.inclination * DEG, out.node * DEG,
659
+ out.perihelion * DEG, points(out.heliocentric), points(out.geocentric))
660
+
661
+
662
+ # ---- eclipses ------------------------------------------------------------------------
663
+
664
+
665
+ def solar_eclipse(jd_tt: float, direction: int = 1) -> Optional[SolarEclipse]:
666
+ """The next (or previous, direction -1) solar eclipse anywhere; TT."""
667
+ o = _n.SolarEclipse()
668
+ if _lib.eph_solar_eclipse_next(jd_tt, direction, byref(o)) != 0:
669
+ return None
670
+ return SolarEclipse(EclipseType(o.type), o.maximum, o.gamma, o.magnitude, o.latitude * DEG,
671
+ o.longitude * DEG, o.saros, o.partial_begin, o.partial_end,
672
+ o.central_begin, o.central_end, o.center_line_begin, o.center_line_end)
673
+
674
+
675
+ def solar_eclipse_local(jd_tt: float, lon: float, lat: float, elevation: float = 0.0,
676
+ direction: int = 1) -> Optional[LocalSolarEclipse]:
677
+ """The next solar eclipse seen from a place (check ``altitude``: the Sun
678
+ may be below the horizon)."""
679
+ o = _n.LocalSolarEclipse()
680
+ obs = _observer(lon, lat, elevation)
681
+ if _lib.eph_solar_eclipse_local(jd_tt, byref(obs), direction, byref(o)) != 0:
682
+ return None
683
+ return LocalSolarEclipse(EclipseType(o.type), o.maximum, o.first_contact, o.second_contact,
684
+ o.third_contact, o.fourth_contact, o.magnitude, o.obscuration,
685
+ o.altitude * DEG, o.saros)
686
+
687
+
688
+ def solar_eclipse_where(jd_tt: float) -> Optional[GeoPoint]:
689
+ """Where the shadow axis meets the Earth; None if it misses."""
690
+ lat, lon = c_double(), c_double()
691
+ if _lib.eph_solar_eclipse_where(jd_tt, byref(lat), byref(lon)) != 0:
692
+ return None
693
+ return GeoPoint(lat.value * DEG, lon.value * DEG)
694
+
695
+
696
+ def solar_eclipse_how(jd_tt: float, lon: float, lat: float,
697
+ elevation: float = 0.0) -> Optional[LocalEclipseState]:
698
+ """The solar eclipse seen from a place at an instant (type 0: none)."""
699
+ o = _n.LocalEclipseState()
700
+ obs = _observer(lon, lat, elevation)
701
+ if _lib.eph_solar_eclipse_how(jd_tt, byref(obs), byref(o)) != 0:
702
+ return None
703
+ return LocalEclipseState(EclipseType(o.type), o.magnitude, o.obscuration, o.altitude * DEG)
704
+
705
+
706
+ def lunar_eclipse(jd_tt: float, direction: int = 1) -> Optional[LunarEclipse]:
707
+ """The next (or previous) lunar eclipse, penumbral ones included; TT."""
708
+ o = _n.LunarEclipse()
709
+ if _lib.eph_lunar_eclipse_next(jd_tt, direction, byref(o)) != 0:
710
+ return None
711
+ return LunarEclipse(EclipseType(o.type), o.maximum, o.gamma, o.umbral_magnitude,
712
+ o.penumbral_magnitude, o.saros, o.penumbral_begin, o.penumbral_end,
713
+ o.partial_begin, o.partial_end, o.total_begin, o.total_end)
714
+
715
+
716
+ # ---- minor planets ------------------------------------------------------------------
717
+
718
+
719
+ def minor_body(name: Union[str, int]) -> Optional[int]:
720
+ """The body id of a loaded minor planet: 433, "433", "Eros" or "433 Eros";
721
+ None if none is loaded. Works with position(), searches, phenomena(),
722
+ orbit() and heliocentric()."""
723
+ i = _lib.eph_minor_find(str(name).encode("utf-8"))
724
+ return None if i < 0 else MINOR_BASE + i
725
+
726
+
727
+ def minor_count() -> int:
728
+ return _lib.eph_minor_count()
729
+
730
+
731
+ def minor_name(body: int) -> str:
732
+ return _str(_lib.eph_minor_name(body - MINOR_BASE))
733
+
734
+
735
+ def minor_list() -> List[MinorPlanet]:
736
+ return [MinorPlanet(MINOR_BASE + i, _lib.eph_minor_number(i), _str(_lib.eph_minor_name(i)))
737
+ for i in range(_lib.eph_minor_count())]
738
+
739
+
740
+ # ---- fixed stars ------------------------------------------------------------------
741
+
742
+
743
+ def star_find(name: str) -> Optional[int]:
744
+ """Star index by name ("Regulus"), designation ("alf Leo") or "HIP 49669";
745
+ None if unknown (or the stars are not loaded)."""
746
+ i = _lib.eph_star_find(name.encode("utf-8"))
747
+ return None if i < 0 else i
748
+
749
+
750
+ def star_count() -> int:
751
+ """Stars loaded (0 without eph-stars.bin); indices 0 .. count - 1."""
752
+ return _lib.eph_star_count()
753
+
754
+
755
+ def star_name(index: int) -> str:
756
+ return _str(_lib.eph_star_name(index))
757
+
758
+
759
+ def star_designation(index: int) -> str:
760
+ return _str(_lib.eph_star_designation(index))
761
+
762
+
763
+ def star_magnitude(index: int) -> float:
764
+ return _lib.eph_star_magnitude(index)
765
+
766
+
767
+ def star_position(index: int, jd_ut: float, year: Optional[float] = None) -> Optional[Position]:
768
+ out = _n.Position()
769
+ rc = _lib.eph_star_position_apparent_ut(index, jd_ut, _year(jd_ut) if year is None else year,
770
+ byref(out))
771
+ return _position(out) if rc == 0 else None
772
+
773
+
774
+ # ---- ayanamsas --------------------------------------------------------------------
775
+
776
+
777
+ def ayanamsa(system: int, jd_tt: float) -> float:
778
+ """Ayanamsa in degrees: sidereal = tropical - ayanamsa."""
779
+ return _lib.eph_ayanamsa(system, jd_tt) * DEG
780
+
781
+
782
+ def ayanamsa_custom(jd_t0: float, value_at_t0: float, jd_tt: float) -> float:
783
+ """A user-defined ayanamsa (degrees at jd_t0) carried by the precession."""
784
+ return _lib.eph_ayanamsa_custom(jd_t0, value_at_t0 * _RAD, jd_tt) * DEG
785
+
786
+
787
+ def ayanamsa_lahiri(jd_tt: float) -> float:
788
+ return _lib.eph_ayanamsa_lahiri(jd_tt) * DEG
789
+
790
+
791
+ def ayanamsa_name(system: int) -> str:
792
+ """'Lahiri', 'Fagan/Bradley', ..."""
793
+ return _str(_lib.eph_ayanamsa_name(system))
794
+
795
+
796
+ # ---- utilities ---------------------------------------------------------------------
797
+
798
+
799
+ def midpoint(a: float, b: float) -> float:
800
+ """Midpoint of the shorter arc between two longitudes, degrees."""
801
+ return _lib.eph_midpoint(a * _RAD, b * _RAD) * DEG
802
+
803
+
804
+ def split_longitude(lon: float) -> SplitLongitude:
805
+ """Zodiac sign (0 = Aries .. 11) and the position within it."""
806
+ sign, deg, minute, sec = c_int(), c_int(), c_int(), c_double()
807
+ _lib.eph_split_longitude(lon * _RAD, byref(sign), byref(deg), byref(minute), byref(sec))
808
+ return SplitLongitude(sign.value, deg.value, minute.value, sec.value)
809
+
ephemera/_native.py ADDED
@@ -0,0 +1,165 @@
1
+ """The native library and its C types (ctypes). Internal: use the functions
2
+ in the ``ephemera`` package, which take and return degrees."""
3
+
4
+ import ctypes
5
+ import os
6
+ import sys
7
+ from ctypes import POINTER, Structure, c_char_p, c_double, c_int, c_size_t, c_void_p
8
+
9
+ _HERE = os.path.dirname(os.path.abspath(__file__))
10
+
11
+
12
+ def _find_library() -> str:
13
+ if sys.platform == "win32":
14
+ names = ["ephemera_native.dll", "libephemera_native.dll"]
15
+ elif sys.platform == "darwin":
16
+ names = ["libephemera_native.dylib", "libephemera_native.so"]
17
+ else:
18
+ names = ["libephemera_native.so"]
19
+ for name in names:
20
+ path = os.path.join(_HERE, name)
21
+ if os.path.exists(path):
22
+ return path
23
+ raise ImportError(f"ephemera: native library not found in {_HERE} (looked for {', '.join(names)})")
24
+
25
+
26
+ lib = ctypes.CDLL(_find_library())
27
+
28
+
29
+ class Position(Structure):
30
+ _fields_ = [(n, c_double) for n in ("longitude", "latitude", "distance",
31
+ "longitude_speed", "latitude_speed", "distance_speed")]
32
+
33
+
34
+ class Observer(Structure):
35
+ _fields_ = [("longitude", c_double), ("latitude", c_double), ("elevation", c_double)]
36
+
37
+
38
+ class Angles(Structure):
39
+ _fields_ = [(n, c_double) for n in ("ascendant", "mc", "armc", "vertex", "equatorial_ascendant",
40
+ "co_ascendant_koch", "co_ascendant_munkasey",
41
+ "polar_ascendant")]
42
+
43
+
44
+ class Phenomena(Structure):
45
+ _fields_ = [(n, c_double) for n in ("phase_angle", "phase", "elongation", "diameter", "magnitude")]
46
+
47
+
48
+ class SolarEclipse(Structure):
49
+ _fields_ = [("type", c_int), ("maximum", c_double), ("gamma", c_double), ("magnitude", c_double),
50
+ ("latitude", c_double), ("longitude", c_double), ("saros", c_int),
51
+ ("partial_begin", c_double), ("partial_end", c_double),
52
+ ("central_begin", c_double), ("central_end", c_double),
53
+ ("center_line_begin", c_double), ("center_line_end", c_double)]
54
+
55
+
56
+ class LocalEclipseState(Structure):
57
+ _fields_ = [("type", c_int), ("magnitude", c_double), ("obscuration", c_double),
58
+ ("altitude", c_double)]
59
+
60
+
61
+ class LocalSolarEclipse(Structure):
62
+ _fields_ = [("type", c_int), ("maximum", c_double),
63
+ ("first_contact", c_double), ("second_contact", c_double),
64
+ ("third_contact", c_double), ("fourth_contact", c_double),
65
+ ("magnitude", c_double), ("obscuration", c_double), ("altitude", c_double),
66
+ ("saros", c_int)]
67
+
68
+
69
+ class LunarEclipse(Structure):
70
+ _fields_ = [("type", c_int), ("maximum", c_double), ("gamma", c_double),
71
+ ("umbral_magnitude", c_double), ("penumbral_magnitude", c_double), ("saros", c_int),
72
+ ("penumbral_begin", c_double), ("penumbral_end", c_double),
73
+ ("partial_begin", c_double), ("partial_end", c_double),
74
+ ("total_begin", c_double), ("total_end", c_double)]
75
+
76
+
77
+ class Orbit(Structure):
78
+ _fields_ = [("semi_major_axis", c_double), ("eccentricity", c_double), ("inclination", c_double),
79
+ ("node", c_double), ("perihelion", c_double),
80
+ ("heliocentric", Position * 4), ("geocentric", Position * 4)]
81
+
82
+
83
+ D = c_double
84
+ I = c_int
85
+ PD = POINTER(c_double)
86
+ PI = POINTER(c_int)
87
+ PPOS = POINTER(Position)
88
+ POBS = POINTER(Observer)
89
+
90
+ _SIGNATURES = {
91
+ # time
92
+ "eph_julian_date": (D, [I, I, D]),
93
+ "eph_calendar_date": (None, [D, PI, PI, PD]),
94
+ "eph_delta_t": (D, [D]),
95
+ "eph_tdb_minus_tt": (D, [D]),
96
+ "eph_ut_to_tt": (D, [D, D]),
97
+ "eph_tt_to_ut": (D, [D, D]),
98
+ "eph_utc_to_jd": (I, [I, I, I, I, I, D, PD, PD]),
99
+ "eph_sidereal_time": (D, [D]),
100
+ "eph_equation_of_time": (I, [D, PD]),
101
+ # frames
102
+ "eph_mean_obliquity": (D, [D]),
103
+ "eph_nutation": (None, [D, PD, PD]),
104
+ "eph_ecliptic_to_equatorial": (None, [D, D, D, PD, PD]),
105
+ # positions
106
+ "eph_position_tt": (I, [I, D, PPOS]),
107
+ "eph_position_apparent_tt": (I, [I, D, PPOS]),
108
+ "eph_position_ut": (I, [I, D, D, PPOS]),
109
+ "eph_position_apparent_ut": (I, [I, D, D, PPOS]),
110
+ "eph_heliocentric_tt": (I, [I, D, PD, PD, PD]),
111
+ "eph_position_topocentric_ut": (I, [I, D, D, POBS, PPOS]),
112
+ "eph_body_name": (c_char_p, [I]),
113
+ # sidereal zodiac and houses
114
+ "eph_ayanamsa": (D, [I, D]),
115
+ "eph_ayanamsa_name": (c_char_p, [I]),
116
+ "eph_ayanamsa_custom": (D, [D, D, D]),
117
+ "eph_ayanamsa_lahiri": (D, [D]),
118
+ "eph_asc_mc": (I, [D, POBS, PD, PD]),
119
+ "eph_houses": (I, [D, POBS, I, PD]),
120
+ "eph_house_angles": (I, [D, POBS, POINTER(Angles)]),
121
+ "eph_house_position": (D, [PD, D]),
122
+ # phenomena, eclipses
123
+ "eph_phenomena_tt": (I, [I, D, POINTER(Phenomena)]),
124
+ "eph_solar_eclipse_next": (I, [D, I, POINTER(SolarEclipse)]),
125
+ "eph_solar_eclipse_where": (I, [D, PD, PD]),
126
+ "eph_solar_eclipse_how": (I, [D, POBS, POINTER(LocalEclipseState)]),
127
+ "eph_solar_eclipse_local": (I, [D, POBS, I, POINTER(LocalSolarEclipse)]),
128
+ "eph_lunar_eclipse_next": (I, [D, I, POINTER(LunarEclipse)]),
129
+ # searches
130
+ "eph_longitude_crossing": (I, [I, D, D, I, D, PD]),
131
+ "eph_longitude_crossing_ut": (I, [I, D, D, I, D, PD]),
132
+ "eph_heliocentric_crossing": (I, [I, D, D, I, D, PD]),
133
+ "eph_longitude_crossing_sidereal": (I, [I, D, I, D, I, D, PD]),
134
+ "eph_longitude_crossing_sidereal_custom": (I, [I, D, D, D, D, I, D, PD]),
135
+ "eph_heliocentric_crossing_sidereal": (I, [I, D, I, D, I, D, PD]),
136
+ "eph_heliocentric_crossing_sidereal_custom": (I, [I, D, D, D, D, I, D, PD]),
137
+ "eph_node_crossing": (I, [I, D, I, D, PD, PD]),
138
+ "eph_station": (I, [I, D, I, D, PD]),
139
+ # minor planets, orbits
140
+ "eph_minor_count": (I, []),
141
+ "eph_minor_find": (I, [c_char_p]),
142
+ "eph_minor_number": (I, [I]),
143
+ "eph_minor_name": (c_char_p, [I]),
144
+ "eph_orbit_tt": (I, [I, D, I, POINTER(Orbit)]),
145
+ # utilities
146
+ "eph_midpoint": (D, [D, D]),
147
+ "eph_split_longitude": (None, [D, PI, PI, PI, PD]),
148
+ # fixed stars
149
+ "eph_star_count": (I, []),
150
+ "eph_star_find": (I, [c_char_p]),
151
+ "eph_star_name": (c_char_p, [I]),
152
+ "eph_star_designation": (c_char_p, [I]),
153
+ "eph_star_magnitude": (D, [I]),
154
+ "eph_star_position_apparent_ut": (I, [I, D, D, PPOS]),
155
+ # data files
156
+ "eph_load_data": (I, [c_void_p, c_size_t]),
157
+ "eph_load_file": (I, [c_char_p]),
158
+ "eph_load_embedded": (I, []),
159
+ "eph_unload_data": (None, []),
160
+ }
161
+
162
+ for _name, (_restype, _argtypes) in _SIGNATURES.items():
163
+ _fn = getattr(lib, _name)
164
+ _fn.restype = _restype
165
+ _fn.argtypes = _argtypes
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
ephemera/py.typed ADDED
File without changes
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyephemera
3
+ Version: 0.4.1
4
+ Summary: Pocket-sized ephemeris for astrology and astronomy: planets, Moon, Chiron, nodes, houses, ayanamsas, stars, eclipses, Uranian points and 1,000+ asteroids. No dependencies, MIT.
5
+ Keywords: ephemeris,astrology,astronomy,planets,houses,ayanamsa,sidereal,eclipse
6
+ Author: Intellecat
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: C++
15
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
16
+ Classifier: Typing :: Typed
17
+ Project-URL: Homepage, https://github.com/intellecat/ephemera
18
+ Project-URL: Documentation, https://github.com/intellecat/ephemera/blob/main/docs/guide.md
19
+ Project-URL: Issues, https://github.com/intellecat/ephemera/issues
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center"><img alt="Ephemera" src="https://raw.githubusercontent.com/intellecat/ephemera/main/assets/logo/lockup-horizontal-light.svg" width="420"></p>
24
+
25
+ # pyephemera
26
+
27
+ **A pocket-sized, MIT-licensed ephemeris for astrology and astronomy.**
28
+ JPL-grade planetary positions, houses, ayanamsas, eclipses, fixed stars and
29
+ 1,000+ asteroids, from a small C library with no dependencies. Wheels for
30
+ Linux, macOS and Windows; Python 3.9+.
31
+
32
+ - **MIT licensed:** use it in closed-source and commercial apps.
33
+ - **Accurate:** within 0.6″ of NASA JPL's DE440 over 1550–2650 with the
34
+ built-in data, or 0.03″ with the high-precision tables.
35
+ - **Small:** the planets, the Moon and Chiron are built in (204 KB); more
36
+ data files come with the package.
37
+ - **Complete for astrology:** 14 house systems, 42 ayanamsas, nodes and
38
+ Lilith, Uranian points, eclipses, returns and stations, sidereal
39
+ returns, planetary nodes and apsides, and 411 fixed stars.
40
+ - **Independently verified** against JPL, NASA's eclipse canon and
41
+ India's official ephemeris. See the
42
+ [accuracy page](https://github.com/intellecat/ephemera/blob/main/docs/accuracy.md).
43
+
44
+ ```sh
45
+ pip install pyephemera
46
+ ```
47
+
48
+ ```python
49
+ import ephemera as eph
50
+
51
+ jd = eph.julian_date(2026, 1, 1.5) # 1 Jan 2026, 12:00 UT
52
+ moon = eph.position(eph.Body.MOON, jd) # degrees, AU, per day
53
+ cusps = eph.houses(jd, -0.1278, 51.5074, eph.HouseSystem.PLACIDUS) # London
54
+ lahiri = eph.ayanamsa(eph.Ayanamsa.LAHIRI, eph.ut_to_tt(jd)) # sidereal = tropical − ayanamsa
55
+ eclipse = eph.solar_eclipse(eph.ut_to_tt(jd)) # the next solar eclipse
56
+ ```
57
+
58
+ ## Times
59
+
60
+ Positions take a **UT** Julian day; searches, eclipses and orbits take
61
+ **TT**. `utc_to_jd` converts civil time (with leap seconds) and accepts a
62
+ `datetime`:
63
+
64
+ ```python
65
+ from datetime import datetime
66
+ from zoneinfo import ZoneInfo
67
+
68
+ jd_tt, jd_ut = eph.utc_to_jd(datetime(1990, 6, 15, 14, 30, tzinfo=ZoneInfo("Europe/London")))
69
+ ```
70
+
71
+ ## Data files
72
+
73
+ The 1″ series with Chiron is built in. The package also carries:
74
+
75
+ ```python
76
+ eph.load_package_data("stars") # 411 named stars, star-based ayanamsas
77
+ eph.load_package_data("asteroids") # Ceres, Pallas, Juno, Vesta (1600–2500)
78
+ eph.load_package_data("01as") # the 0.2″ series
79
+ eph.load_data("de440-2000.bin") # any file from the repository, by path or bytes
80
+ ```
81
+
82
+ The DE440 tables (0.002″) and the minor-planet files (1,000+ asteroids,
83
+ 1900–2100) are downloads from the
84
+ [repository](https://github.com/intellecat/ephemera/blob/main/docs/data.md).
85
+ Loaded data is shared by the whole process.
86
+
87
+ ## Documentation
88
+
89
+ The [guide](https://github.com/intellecat/ephemera/blob/main/docs/guide.md)
90
+ covers every feature. The Python names follow the JavaScript API in
91
+ snake_case: `houses_info`, `longitude_crossing`, `solar_eclipse_local`,
92
+ `minor_body`; calls that find nothing return `None`.
93
+
94
+ ## License
95
+
96
+ MIT. Built from public data: NASA JPL, NASA's eclipse catalogues and
97
+ Hipparcos. No third-party code is included.
@@ -0,0 +1,15 @@
1
+ ephemera/__init__.py,sha256=cjbHg4N2T2VG4P_BM3tod08Erx-eIgCkff7BEUaj9sk,26155
2
+ ephemera/_native.py,sha256=6XBqze1dtw2rCc6pGiTwF6DdiyVkOdgI_17xipwPVvA,6545
3
+ ephemera/data/eph-01as-chiron.bin,sha256=wSNH46BEh-_yUytKW3sY1MdbE5nKkTURLzsuEaF4V4w,362108
4
+ ephemera/data/eph-01as.bin,sha256=XAK95lbyfxlkEHhD9yYHwv1HEnHLPj069QenOOjl9yI,302536
5
+ ephemera/data/eph-1as-chiron.bin,sha256=yj0-d5zwajplkr9PR-ht_gujzL9Mn6H_sjF6jpxE12s,204292
6
+ ephemera/data/eph-1as.bin,sha256=eVmz3dZ9tBF8aEVyxfmY4oLx6yMY91FZZO919HGbM-k,144720
7
+ ephemera/data/eph-asteroids.bin,sha256=mrlHhLU294RC1de0bdnOS1a0JYwaSqiuKQAifQchZLA,424916
8
+ ephemera/data/eph-chiron.bin,sha256=lW-KRjR93Q94gbTRxqduAvwAHAB5SJE1Msd7qrUu2ng,59644
9
+ ephemera/data/eph-stars.bin,sha256=9Tjq2gjkQn9Hsle5k_jt7TgQgTohAiQdhhTSQ-pLVlE,25853
10
+ ephemera/ephemera_native.dll,sha256=Vi6DoYij5smQ2gcwS1kDds6Wo46J7N9sRFoir3CHcHI,336384
11
+ ephemera/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pyephemera-0.4.1.dist-info/METADATA,sha256=P4xdfYMHjVCrBHsnILROtgrzH4-Kz1MGPd-eEv5_2MA,4034
13
+ pyephemera-0.4.1.dist-info/WHEEL,sha256=H2-vNj0Uj93L_UlwWLAHSbPwt0nxqZmlgAGZZjm-2qY,102
14
+ pyephemera-0.4.1.dist-info/licenses/LICENSE,sha256=kp2mYkUsVBt5ZBrgioeyEdl_vuEdEZ-dvMZEoWeFBAY,1067
15
+ pyephemera-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 1.1.1
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Intellecat
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.