natal-chart 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ *.bsp
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - unreleased
4
+
5
+ First release: planets Sun-Pluto (JPL DE440s via Skyfield), mean node, mean
6
+ Lilith, Placidus/Koch/Porphyry/Equal/Whole Sign houses with a polar-latitude
7
+ error or fallback, major aspects with applying/separating, transits, CLI.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Balakirev
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.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.5
2
+ Name: natal-chart
3
+ Version: 0.1.0
4
+ Summary: Tropical natal chart math (planets, Placidus/Koch/whole-sign houses, aspects, transits) on NASA JPL ephemerides via Skyfield. No Swiss Ephemeris.
5
+ Project-URL: Homepage, https://astralread.com
6
+ Project-URL: Repository, https://github.com/VictorMachiavelli/natal-chart
7
+ Project-URL: Issues, https://github.com/VictorMachiavelli/natal-chart/issues
8
+ Author-email: Victor Balakirev <balakirevvictor@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: aspects,astrology,ephemeris,horoscope,houses,jpl,natal chart,placidus,skyfield,transits
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: skyfield>=1.45
27
+ Requires-Dist: tzdata; sys_platform == 'win32'
28
+ Provides-Extra: test
29
+ Requires-Dist: pyswisseph>=2.10; extra == 'test'
30
+ Requires-Dist: pytest>=7; extra == 'test'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # natal-chart
34
+
35
+ Tropical natal chart math for Python: planet positions, houses
36
+ (Placidus, Koch, Porphyry, Equal, Whole Sign), Ascendant/MC, mean lunar node,
37
+ mean Black Moon Lilith, major aspects and transits.
38
+
39
+ Positions come from NASA JPL ephemerides through
40
+ [Skyfield](https://rhodesmill.org/skyfield/). The package does **not** use or
41
+ contain the Swiss Ephemeris, so it can be released under the MIT license.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pip install natal-chart
47
+ ```
48
+
49
+ Requires Python 3.10+. The only dependency is Skyfield (which brings NumPy).
50
+
51
+ The first call downloads the JPL DE440s ephemeris (~32 MB, covers
52
+ 1849-2150) into `~/.cache/natal-chart` (override with the `NATAL_CHART_DATA`
53
+ environment variable). It is never bundled with the package. To use a
54
+ different file, e.g. the smaller DE421 (1900-2050) or DE440 (1550-2650):
55
+
56
+ ```python
57
+ import natal_chart
58
+ natal_chart.set_ephemeris("de421.bsp") # or NATAL_CHART_EPHEMERIS=de421.bsp
59
+ ```
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from natal_chart import chart, aspects, transits
65
+
66
+ # Fictional birth data: local time + IANA time zone
67
+ c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
68
+
69
+ c["Sun"].sign, round(c["Sun"].degree, 2) # ('Taurus', 26.26)
70
+ c["Saturn"].retrograde # True
71
+ c["Moon"].house # 5
72
+ c.ascendant.longitude, c.mc.longitude # 166.69..., 71.52...
73
+ c.cusps # 12 cusp longitudes, cusp 1 first
74
+
75
+ for a in aspects(c)[:3]:
76
+ print(a.first, a.aspect, a.second, round(a.orb, 2),
77
+ "applying" if a.applying else "separating")
78
+
79
+ for a in transits(c, "2026-11-10T12:00:00+00:00"):
80
+ print("transiting", a.first, a.aspect, "natal", a.second, round(a.orb, 2))
81
+ ```
82
+
83
+ `chart()` accepts a timezone-aware `datetime`, or a naive one plus `tz=`.
84
+ A naive time without `tz` is rejected rather than silently treated as UTC.
85
+
86
+ Points: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune,
87
+ Pluto, `"Mean Node"`, `"Lilith"`, `"Ascendant"`, `"MC"`. Each `Point` has
88
+ `longitude`, `latitude`, `speed` (°/day), `sign`, `degree` (within sign),
89
+ `retrograde` and `house`.
90
+
91
+ ### Aspects
92
+
93
+ `aspects(chart, orbs=None, points=None)` finds conjunction, sextile, square,
94
+ trine and opposition, tightest first. Default orbs:
95
+ conjunction/opposition 8°, trine/square 7°, sextile 5°. Pass your own
96
+ mapping, e.g. `orbs={"conjunction": 10, "square": 6}` (aspects left out are
97
+ not searched). `applying` is decided from the current speeds of both points.
98
+
99
+ `transits(natal, when, orbs=None)` compares the transiting planets with every
100
+ natal point (default orb 1°). The natal point is treated as fixed.
101
+
102
+ ### Command line
103
+
104
+ ```text
105
+ $ natal-chart 1990-05-17T14:30 --tz Europe/Moscow --lat 55.75 --lon 37.62 --aspects
106
+ 1990-05-17 10:30:00 UTC lat 55.7500 lon 37.6200 houses: placidus
107
+
108
+ Point Sign Degree Longitude Speed R House
109
+ Sun Taurus 26°15'49" 56.2636 0.9636 9
110
+ Moon Aquarius 21°37'13" 321.6204 12.9387 5
111
+ ...
112
+ ```
113
+
114
+ Options: `--houses {placidus,koch,porphyry,equal,whole_sign}`,
115
+ `--polar-fallback {porphyry,equal,whole_sign}`, `--aspects`,
116
+ `--transits DATETIME`, `--json`. Without `--tz` the time is taken as UTC.
117
+
118
+ ## What exactly is computed
119
+
120
+ * **Planets**: geocentric *apparent* positions (light-time, aberration,
121
+ gravitational deflection), ecliptic longitude and latitude referred to the
122
+ **true equinox and ecliptic of date** (IAU 2006 precession + IAU 2000A
123
+ nutation), tropical zodiac. This is the same convention as the default
124
+ output of most astrology software. Mars-Pluto use the planetary-system
125
+ barycenter segments of the JPL file; the offset from the planet's centre
126
+ is below 0.1" as seen from Earth.
127
+ * **Speed**: central difference over ±0.01 day, degrees per day.
128
+ * **Mean node / mean Lilith**: ELP-2000/82 mean-element polynomials
129
+ (Meeus, *Astronomical Algorithms*, ch. 47), plus nutation in longitude.
130
+ Lilith (mean lunar apogee) is projected from the inclined lunar orbit onto
131
+ the ecliptic, so it also has a small ecliptic latitude.
132
+ * **Houses**: RAMC from Skyfield's Greenwich apparent sidereal time plus the
133
+ east longitude; true obliquity of date. Placidus is computed by iterating
134
+ each cusp on its own diurnal/nocturnal semi-arc; Koch from Ascendants at
135
+ the times that trisect the MC degree's semi-arc.
136
+ * **Time**: from 1972 the input is UTC with leap seconds. Before 1972 the
137
+ given UTC is treated as UT1 (civil time followed UT then); Delta T comes
138
+ from Skyfield's built-in model. Ambiguous or non-existent local times at
139
+ DST changes follow Python's `datetime.fold` rule (`fold=0` by default).
140
+
141
+ ### High latitudes
142
+
143
+ Placidus and Koch are undefined when part of the ecliptic never rises or sets,
144
+ i.e. for |latitude| ≥ 90° − obliquity (about 66.56°). There `chart()` raises
145
+ `PolarHousesError` with an explanation. Pass
146
+ `polar_fallback="porphyry"` (or `"equal"`, `"whole_sign"`) to get a chart
147
+ anyway; then `chart.house_system` names the system actually used and
148
+ `chart.polar_fallback_used` is `True`. Equal, Whole Sign and Porphyry work at
149
+ any latitude short of the poles.
150
+
151
+ ## Accuracy
152
+
153
+ Measured against the Swiss Ephemeris 2.10 (pyswisseph, with its
154
+ `sepl_18`/`semo_18` data files) on 300 random fictional dates 1900-2100 and
155
+ latitudes within ±66°. The comparison is run at identical instants (same TT
156
+ for bodies, same UT1 for houses) so that what is measured is ephemeris,
157
+ reduction and house math, not the two libraries' Delta T models.
158
+
159
+ | Quantity | Max difference |
160
+ | --- | --- |
161
+ | Sun ... Pluto, longitude | 0.007" |
162
+ | Sun ... Pluto, latitude | 0.003" |
163
+ | Speeds | 0.00008 °/day |
164
+ | Mean Node | 0.11" |
165
+ | Mean Lilith (longitude / latitude) | 0.54" / 0.04" |
166
+ | Ascendant, MC | 0.000003° |
167
+ | Placidus, Koch, Porphyry, Equal cusps | 0.000006° |
168
+
169
+ Things that differ between libraries and are *not* in the table:
170
+
171
+ * **Delta T for future dates.** Neither library can know future Earth
172
+ rotation; their predictions differ by a few seconds after 2026, which moves
173
+ the Moon (0.5"/s) by up to ~13" at the same civil time (we measured 0.003"
174
+ for 1972-2025 and 0.4" for 1900-1971).
175
+ * **Sidereal time after 2050.** From 2050-01-01 the Swiss Ephemeris switches
176
+ to its own long-term sidereal-time formula, which differs from the IERS
177
+ expression used here by ~1.9" (0.13 s of time).
178
+
179
+ The comparison test (`tests/test_vs_swisseph.py`) runs only when the
180
+ optional `pyswisseph` dev dependency is installed; point `SE_EPHE_PATH` at the
181
+ Swiss data files to compare against the full Swiss Ephemeris instead of its
182
+ built-in Moshier fallback.
183
+
184
+ ## Ephemeris data
185
+
186
+ NASA JPL DE440 (public domain) via Skyfield. Default file DE440s; the
187
+ file is downloaded on first use and is not part of this package.
188
+
189
+ ## Development
190
+
191
+ ```bash
192
+ python -m venv .venv && .venv/bin/pip install -e ".[test]"
193
+ .venv/bin/pytest -s
194
+ ```
195
+
196
+ All birth data in the tests is fictional. `pyswisseph` (AGPL) appears only in
197
+ the optional test extra and is never imported by the package.
198
+
199
+ ## License
200
+
201
+ MIT. See [LICENSE](LICENSE).
202
+
203
+ ---
204
+
205
+ Built by the team behind [AstralRead](https://astralread.com) — free natal
206
+ chart & AI astrologer.
@@ -0,0 +1,174 @@
1
+ # natal-chart
2
+
3
+ Tropical natal chart math for Python: planet positions, houses
4
+ (Placidus, Koch, Porphyry, Equal, Whole Sign), Ascendant/MC, mean lunar node,
5
+ mean Black Moon Lilith, major aspects and transits.
6
+
7
+ Positions come from NASA JPL ephemerides through
8
+ [Skyfield](https://rhodesmill.org/skyfield/). The package does **not** use or
9
+ contain the Swiss Ephemeris, so it can be released under the MIT license.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install natal-chart
15
+ ```
16
+
17
+ Requires Python 3.10+. The only dependency is Skyfield (which brings NumPy).
18
+
19
+ The first call downloads the JPL DE440s ephemeris (~32 MB, covers
20
+ 1849-2150) into `~/.cache/natal-chart` (override with the `NATAL_CHART_DATA`
21
+ environment variable). It is never bundled with the package. To use a
22
+ different file, e.g. the smaller DE421 (1900-2050) or DE440 (1550-2650):
23
+
24
+ ```python
25
+ import natal_chart
26
+ natal_chart.set_ephemeris("de421.bsp") # or NATAL_CHART_EPHEMERIS=de421.bsp
27
+ ```
28
+
29
+ ## Quickstart
30
+
31
+ ```python
32
+ from natal_chart import chart, aspects, transits
33
+
34
+ # Fictional birth data: local time + IANA time zone
35
+ c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
36
+
37
+ c["Sun"].sign, round(c["Sun"].degree, 2) # ('Taurus', 26.26)
38
+ c["Saturn"].retrograde # True
39
+ c["Moon"].house # 5
40
+ c.ascendant.longitude, c.mc.longitude # 166.69..., 71.52...
41
+ c.cusps # 12 cusp longitudes, cusp 1 first
42
+
43
+ for a in aspects(c)[:3]:
44
+ print(a.first, a.aspect, a.second, round(a.orb, 2),
45
+ "applying" if a.applying else "separating")
46
+
47
+ for a in transits(c, "2026-11-10T12:00:00+00:00"):
48
+ print("transiting", a.first, a.aspect, "natal", a.second, round(a.orb, 2))
49
+ ```
50
+
51
+ `chart()` accepts a timezone-aware `datetime`, or a naive one plus `tz=`.
52
+ A naive time without `tz` is rejected rather than silently treated as UTC.
53
+
54
+ Points: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune,
55
+ Pluto, `"Mean Node"`, `"Lilith"`, `"Ascendant"`, `"MC"`. Each `Point` has
56
+ `longitude`, `latitude`, `speed` (°/day), `sign`, `degree` (within sign),
57
+ `retrograde` and `house`.
58
+
59
+ ### Aspects
60
+
61
+ `aspects(chart, orbs=None, points=None)` finds conjunction, sextile, square,
62
+ trine and opposition, tightest first. Default orbs:
63
+ conjunction/opposition 8°, trine/square 7°, sextile 5°. Pass your own
64
+ mapping, e.g. `orbs={"conjunction": 10, "square": 6}` (aspects left out are
65
+ not searched). `applying` is decided from the current speeds of both points.
66
+
67
+ `transits(natal, when, orbs=None)` compares the transiting planets with every
68
+ natal point (default orb 1°). The natal point is treated as fixed.
69
+
70
+ ### Command line
71
+
72
+ ```text
73
+ $ natal-chart 1990-05-17T14:30 --tz Europe/Moscow --lat 55.75 --lon 37.62 --aspects
74
+ 1990-05-17 10:30:00 UTC lat 55.7500 lon 37.6200 houses: placidus
75
+
76
+ Point Sign Degree Longitude Speed R House
77
+ Sun Taurus 26°15'49" 56.2636 0.9636 9
78
+ Moon Aquarius 21°37'13" 321.6204 12.9387 5
79
+ ...
80
+ ```
81
+
82
+ Options: `--houses {placidus,koch,porphyry,equal,whole_sign}`,
83
+ `--polar-fallback {porphyry,equal,whole_sign}`, `--aspects`,
84
+ `--transits DATETIME`, `--json`. Without `--tz` the time is taken as UTC.
85
+
86
+ ## What exactly is computed
87
+
88
+ * **Planets**: geocentric *apparent* positions (light-time, aberration,
89
+ gravitational deflection), ecliptic longitude and latitude referred to the
90
+ **true equinox and ecliptic of date** (IAU 2006 precession + IAU 2000A
91
+ nutation), tropical zodiac. This is the same convention as the default
92
+ output of most astrology software. Mars-Pluto use the planetary-system
93
+ barycenter segments of the JPL file; the offset from the planet's centre
94
+ is below 0.1" as seen from Earth.
95
+ * **Speed**: central difference over ±0.01 day, degrees per day.
96
+ * **Mean node / mean Lilith**: ELP-2000/82 mean-element polynomials
97
+ (Meeus, *Astronomical Algorithms*, ch. 47), plus nutation in longitude.
98
+ Lilith (mean lunar apogee) is projected from the inclined lunar orbit onto
99
+ the ecliptic, so it also has a small ecliptic latitude.
100
+ * **Houses**: RAMC from Skyfield's Greenwich apparent sidereal time plus the
101
+ east longitude; true obliquity of date. Placidus is computed by iterating
102
+ each cusp on its own diurnal/nocturnal semi-arc; Koch from Ascendants at
103
+ the times that trisect the MC degree's semi-arc.
104
+ * **Time**: from 1972 the input is UTC with leap seconds. Before 1972 the
105
+ given UTC is treated as UT1 (civil time followed UT then); Delta T comes
106
+ from Skyfield's built-in model. Ambiguous or non-existent local times at
107
+ DST changes follow Python's `datetime.fold` rule (`fold=0` by default).
108
+
109
+ ### High latitudes
110
+
111
+ Placidus and Koch are undefined when part of the ecliptic never rises or sets,
112
+ i.e. for |latitude| ≥ 90° − obliquity (about 66.56°). There `chart()` raises
113
+ `PolarHousesError` with an explanation. Pass
114
+ `polar_fallback="porphyry"` (or `"equal"`, `"whole_sign"`) to get a chart
115
+ anyway; then `chart.house_system` names the system actually used and
116
+ `chart.polar_fallback_used` is `True`. Equal, Whole Sign and Porphyry work at
117
+ any latitude short of the poles.
118
+
119
+ ## Accuracy
120
+
121
+ Measured against the Swiss Ephemeris 2.10 (pyswisseph, with its
122
+ `sepl_18`/`semo_18` data files) on 300 random fictional dates 1900-2100 and
123
+ latitudes within ±66°. The comparison is run at identical instants (same TT
124
+ for bodies, same UT1 for houses) so that what is measured is ephemeris,
125
+ reduction and house math, not the two libraries' Delta T models.
126
+
127
+ | Quantity | Max difference |
128
+ | --- | --- |
129
+ | Sun ... Pluto, longitude | 0.007" |
130
+ | Sun ... Pluto, latitude | 0.003" |
131
+ | Speeds | 0.00008 °/day |
132
+ | Mean Node | 0.11" |
133
+ | Mean Lilith (longitude / latitude) | 0.54" / 0.04" |
134
+ | Ascendant, MC | 0.000003° |
135
+ | Placidus, Koch, Porphyry, Equal cusps | 0.000006° |
136
+
137
+ Things that differ between libraries and are *not* in the table:
138
+
139
+ * **Delta T for future dates.** Neither library can know future Earth
140
+ rotation; their predictions differ by a few seconds after 2026, which moves
141
+ the Moon (0.5"/s) by up to ~13" at the same civil time (we measured 0.003"
142
+ for 1972-2025 and 0.4" for 1900-1971).
143
+ * **Sidereal time after 2050.** From 2050-01-01 the Swiss Ephemeris switches
144
+ to its own long-term sidereal-time formula, which differs from the IERS
145
+ expression used here by ~1.9" (0.13 s of time).
146
+
147
+ The comparison test (`tests/test_vs_swisseph.py`) runs only when the
148
+ optional `pyswisseph` dev dependency is installed; point `SE_EPHE_PATH` at the
149
+ Swiss data files to compare against the full Swiss Ephemeris instead of its
150
+ built-in Moshier fallback.
151
+
152
+ ## Ephemeris data
153
+
154
+ NASA JPL DE440 (public domain) via Skyfield. Default file DE440s; the
155
+ file is downloaded on first use and is not part of this package.
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ python -m venv .venv && .venv/bin/pip install -e ".[test]"
161
+ .venv/bin/pytest -s
162
+ ```
163
+
164
+ All birth data in the tests is fictional. `pyswisseph` (AGPL) appears only in
165
+ the optional test extra and is never imported by the package.
166
+
167
+ ## License
168
+
169
+ MIT. See [LICENSE](LICENSE).
170
+
171
+ ---
172
+
173
+ Built by the team behind [AstralRead](https://astralread.com) — free natal
174
+ chart & AI astrologer.
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "natal-chart"
7
+ dynamic = ["version"]
8
+ description = "Tropical natal chart math (planets, Placidus/Koch/whole-sign houses, aspects, transits) on NASA JPL ephemerides via Skyfield. No Swiss Ephemeris."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Victor Balakirev", email = "balakirevvictor@gmail.com" }]
14
+ keywords = ["astrology", "natal chart", "horoscope", "ephemeris", "placidus", "houses", "aspects", "transits", "skyfield", "jpl"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Scientific/Engineering :: Astronomy",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = [
31
+ "skyfield>=1.45",
32
+ "tzdata; sys_platform == 'win32'",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ # pyswisseph (AGPL) is used ONLY by the optional cross-check tests; it is
37
+ # never imported by the package itself.
38
+ test = ["pytest>=7", "pyswisseph>=2.10"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://astralread.com"
42
+ Repository = "https://github.com/VictorMachiavelli/natal-chart"
43
+ Issues = "https://github.com/VictorMachiavelli/natal-chart/issues"
44
+
45
+ [project.scripts]
46
+ natal-chart = "natal_chart.cli:main"
47
+
48
+ [tool.hatch.version]
49
+ path = "src/natal_chart/__init__.py"
50
+
51
+ [tool.hatch.build.targets.wheel]
52
+ packages = ["src/natal_chart"]
53
+
54
+ [tool.hatch.build.targets.sdist]
55
+ include = ["src/natal_chart", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
@@ -0,0 +1,42 @@
1
+ """natal-chart: tropical natal chart math on NASA JPL ephemerides (via Skyfield).
2
+
3
+ >>> from natal_chart import chart, aspects
4
+ >>> c = chart("1990-05-17T14:30", lat=55.75, lon=37.62, tz="Europe/Moscow")
5
+ >>> c["Sun"].sign
6
+ 'Taurus'
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ from ._ephemeris import data_dir, set_ephemeris
14
+ from .aspects import (
15
+ ASPECT_ANGLES,
16
+ DEFAULT_ORBS,
17
+ DEFAULT_TRANSIT_ORBS,
18
+ Aspect,
19
+ aspects,
20
+ transits,
21
+ )
22
+ from .chart import PLANETS, Chart, Point, chart, positions
23
+ from .houses import HOUSE_SYSTEMS, PolarHousesError
24
+
25
+ __all__ = [
26
+ "ASPECT_ANGLES",
27
+ "DEFAULT_ORBS",
28
+ "DEFAULT_TRANSIT_ORBS",
29
+ "HOUSE_SYSTEMS",
30
+ "PLANETS",
31
+ "Aspect",
32
+ "Chart",
33
+ "Point",
34
+ "PolarHousesError",
35
+ "aspects",
36
+ "chart",
37
+ "data_dir",
38
+ "positions",
39
+ "set_ephemeris",
40
+ "transits",
41
+ "__version__",
42
+ ]
@@ -0,0 +1,69 @@
1
+ """Small angle helpers (degrees)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ SIGNS: tuple[str, ...] = (
8
+ "Aries",
9
+ "Taurus",
10
+ "Gemini",
11
+ "Cancer",
12
+ "Leo",
13
+ "Virgo",
14
+ "Libra",
15
+ "Scorpio",
16
+ "Sagittarius",
17
+ "Capricorn",
18
+ "Aquarius",
19
+ "Pisces",
20
+ )
21
+
22
+
23
+ def norm360(x: float) -> float:
24
+ """Normalise an angle to [0, 360)."""
25
+ x = math.fmod(x, 360.0)
26
+ if x < 0:
27
+ x += 360.0
28
+ # fmod can return 360.0 for tiny negative inputs after the addition
29
+ return 0.0 if x >= 360.0 else x
30
+
31
+
32
+ def norm180(x: float) -> float:
33
+ """Normalise an angle to [-180, 180)."""
34
+ return norm360(x + 180.0) - 180.0
35
+
36
+
37
+ def sind(x: float) -> float:
38
+ return math.sin(math.radians(x))
39
+
40
+
41
+ def cosd(x: float) -> float:
42
+ return math.cos(math.radians(x))
43
+
44
+
45
+ def tand(x: float) -> float:
46
+ return math.tan(math.radians(x))
47
+
48
+
49
+ def asind(x: float) -> float:
50
+ return math.degrees(math.asin(max(-1.0, min(1.0, x))))
51
+
52
+
53
+ def atan2d(y: float, x: float) -> float:
54
+ return math.degrees(math.atan2(y, x))
55
+
56
+
57
+ def sign_of(longitude: float) -> tuple[str, float]:
58
+ """Return (sign name, degree within sign) for an ecliptic longitude."""
59
+ lon = norm360(longitude)
60
+ idx = int(lon // 30.0)
61
+ return SIGNS[idx], lon - 30.0 * idx
62
+
63
+
64
+ def format_dms(degree_in_sign: float) -> str:
65
+ """Format a degree within a sign as ``DD°MM'SS"``."""
66
+ total = int(round(degree_in_sign * 3600.0))
67
+ d, rem = divmod(total, 3600)
68
+ m, s = divmod(rem, 60)
69
+ return f"{d:2d}°{m:02d}'{s:02d}\""
@@ -0,0 +1,61 @@
1
+ """Loading of the JPL ephemeris through Skyfield.
2
+
3
+ The ephemeris file is never shipped with the package. On first use Skyfield
4
+ downloads it into a cache directory:
5
+
6
+ * ``$NATAL_CHART_DATA`` if set, otherwise
7
+ * ``~/.cache/natal-chart``.
8
+
9
+ The default file is NASA JPL DE440s (1849-2150, ~32 MB). Any other
10
+ Skyfield-readable SPK file (for example ``de421.bsp``, 1900-2050, ~17 MB, or
11
+ ``de440.bsp``, 1550-2650, ~114 MB) can be selected with
12
+ :func:`set_ephemeris` or the ``NATAL_CHART_EPHEMERIS`` environment variable.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from functools import lru_cache
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from skyfield.api import Loader
23
+
24
+ DEFAULT_EPHEMERIS = "de440s.bsp"
25
+
26
+ _ephemeris_name: str = os.environ.get("NATAL_CHART_EPHEMERIS", DEFAULT_EPHEMERIS)
27
+
28
+
29
+ def data_dir() -> Path:
30
+ """Directory where the ephemeris file is cached."""
31
+ env = os.environ.get("NATAL_CHART_DATA")
32
+ return Path(env).expanduser() if env else Path.home() / ".cache" / "natal-chart"
33
+
34
+
35
+ @lru_cache(maxsize=1)
36
+ def _loader() -> Loader:
37
+ path = data_dir()
38
+ path.mkdir(parents=True, exist_ok=True)
39
+ return Loader(str(path), verbose=False)
40
+
41
+
42
+ @lru_cache(maxsize=1)
43
+ def timescale() -> Any:
44
+ """Skyfield timescale (uses Skyfield's built-in Delta T / leap-second data)."""
45
+ return _loader().timescale()
46
+
47
+
48
+ @lru_cache(maxsize=4)
49
+ def _load(name: str) -> Any:
50
+ return _loader()(name)
51
+
52
+
53
+ def ephemeris() -> Any:
54
+ """The currently selected JPL ephemeris, downloading it on first use."""
55
+ return _load(_ephemeris_name)
56
+
57
+
58
+ def set_ephemeris(name_or_path: str) -> None:
59
+ """Select the SPK file to use, e.g. ``"de421.bsp"`` or a local path."""
60
+ global _ephemeris_name
61
+ _ephemeris_name = name_or_path