A few weeks ago, I open-sourced the Python calculation engine behind GetBirthChart.
The source was public, but using it still meant cloning the repository, setting up the environment, and understanding the internal package structure.
That is useful if you want to inspect the code.
It is less useful if you just want to use the engine.
So I packaged it properly.
Today, gbc-astro can be installed directly from PyPI:
pip install gbc-astro
The source remains open on GitHub.
But now the calculation engine can also behave like what it really is: a Python library.
I didn't want to create a second engine
This was the most important constraint.
GetBirthChart already had a working calculation engine.
Publishing to PyPI should not create another implementation.
I wanted this:
GetBirthChart
↓
gbc-astro
↑
Python package
Not this:
GetBirthChart → engine A
PyPI → engine B
So the public package is a thin interface around the same calculation code used by GetBirthChart.
A basic chart looks like this:
from gbc_astro import calculate_chart
chart = calculate_chart(
date="1990-05-15",
time="09:30",
latitude=51.5074,
longitude=-0.1278,
timezone="Europe/London",
)
print(chart.bodies["sun"].sign)
print(chart.bodies["moon"].sign)
print(chart.angles)
Underneath that function, the package still delegates to the existing engine.
calculate_chart(...)
↓
AstrologyEngine.natal(...)
↓
calculation providers
↓
Swiss Ephemeris
No separate astrology math was introduced for PyPI.
What the package calculates
The engine currently handles things like:
- planetary positions
- zodiac placements
- houses
- Ascendant and Midheaven
- aspects
- retrograde state
- timezone-aware birth data
- derived chart data
- structured warnings
Swiss Ephemeris is accessed through pyswisseph.
One thing I wanted to preserve exactly was how the engine handles missing information.
Unknown birth time is still unknown
This has been one of the design decisions I care about most in GetBirthChart.
If the birth time is unknown, the library does not silently invent one.
For example:
from gbc_astro import calculate_chart
chart = calculate_chart(
date="1990-05-15",
time=None,
latitude=51.5074,
longitude=-0.1278,
timezone="Europe/London",
)
time=None means the birth time is actually unknown.
The engine does not quietly substitute 12:00.
Time-dependent values are omitted instead.
Conceptually:
birth_time_known = False
angles = {}
houses = ()
rising = None
This matters because the Ascendant and houses depend on birth time.
A library should not turn missing input into fake precision just because a complete-looking result is easier to return.
The HTTP API is optional
GetBirthChart also uses the engine behind a FastAPI service.
But someone installing a calculation library should not need FastAPI and Uvicorn unless they actually want the HTTP layer.
The normal installation is:
pip install gbc-astro
For the API dependencies:
pip install "gbc-astro[api]"
So the architecture stays fairly simple:
Python application
↓
gbc-astro
HTTP service
↓
FastAPI adapter
↓
gbc-astro
Same engine, different interface.
Package version and engine version are not always the same thing
Packaging also forced me to separate two versions that are easy to confuse.
The current package version is:
1.12.2
The calculation engine version is:
1.12.1
That difference is intentional.
Version 1.12.2 was a packaging and distribution release.
It did not change the underlying astrology calculations.
So I did not bump the calculation engine version just to make the numbers look identical.
I would rather have the versions describe what actually changed.
Making a release traceable
Once the package was public, I also wanted a clear path from an installed Python package back to its source.
The project now has three public representations:
GitHub
↓
source code
PyPI
↓
installable package
Zenodo
↓
archived release
The 1.12.2 source release is archived on Zenodo with this DOI:
10.5281/zenodo.22052875
A DOI does not validate the astrology.
It does not prove that a calculation is correct.
It simply gives this particular software release a persistent, citable identifier.
For open-source infrastructure, that is useful.
Someone looking at the package later can trace it back to a specific archived release instead of relying only on whatever happens to be on the repository's main branch at that time.
Packaging exposed assumptions
The interesting part of turning the engine into a package wasn't uploading a wheel.
It was deciding what the public contract should be.
Which functions should be public?
Which exceptions should developers rely on?
What happens when birth time is missing?
Which dependencies belong in the core package?
Should the HTTP layer be installed by default?
Can a clean Python environment reproduce a real chart?
Can a published package be traced back to its source?
Those questions already existed when the engine was private infrastructure.
Publishing it just made them harder to ignore.
And I think that's one of the useful side effects of open sourcing internal systems.
It forces implicit assumptions to become explicit interfaces.
Try it
Install the package:
pip install gbc-astro
PyPI:
https://pypi.org/project/gbc-astro/1.12.2/
Source code:
https://github.com/getbirthchart-com/gbc-astro-engine
Archived release:
https://zenodo.org/records/22052875
DOI:
https://doi.org/10.5281/zenodo.22052875
GetBirthChart:
I'm going to keep documenting the less obvious parts of building this engine — especially time handling, house calculations, testing, uncertainty, and the boundary between deterministic calculations and AI interpretation.
Top comments (0)