diff --git a/README.md b/README.md index a67aa71..be57eb4 100644 --- a/README.md +++ b/README.md @@ -807,3 +807,8 @@ See LICENSE file. Contact ------- https://github.com/naturalstupid/ + +FastAPI Backend +--------------- +A lightweight API implementation using FastAPI is available in `src/astroapi`. Run `uvicorn astroapi.main:app --reload` to start the server. +For a browser-based UI, open `frontend/index.html` with any static file server. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a74fbbd --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,91 @@ + + + + + + AstroCal Chart + + + + + + +
+ + + + diff --git a/requirements.txt b/requirements.txt index 196676c..c215b1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ attr==0.3.2 -conda==4.3.16 geocoder==1.38.1 geopy==2.4.1 img2pdf==0.5.1 @@ -12,10 +11,12 @@ PyQt6_sip==13.8.0 pyqtgraph==0.13.7 pyswisseph==2.10.3.2 pytest==7.4.4 -pytest.egg==info python_dateutil==2.9.0.post0 pytz==2024.1 Requests==2.32.3 -setuptools==69.5.1 setuptools==75.6.0 timezonefinder==6.5.8 + +fastapi==0.111.0 +uvicorn==0.29.0 +pydantic==2.7.1 diff --git a/src/astroapi/README.md b/src/astroapi/README.md new file mode 100644 index 0000000..9fa6c41 --- /dev/null +++ b/src/astroapi/README.md @@ -0,0 +1,31 @@ +# AstroCal FastAPI Backend + +This module exposes horoscope computations via a simple REST API. + +## Endpoints + +- `GET /health` – basic health check +- `POST /chart` or `/generate_chart` – compute a Vedic horoscope + +### Example request + +```json +{ + "name": "John Doe", + "gender": "M", + "birth_date": "2000-01-01", + "birth_time": "12:34:00", + "place": "Chennai, India", + "latitude": 13.0827, + "longitude": 80.2707, + "timezone": 5.5 +} +``` + +Start the API with: + +```bash +uvicorn astroapi.main:app --reload +``` + +The response contains horoscope information ready for client consumption. diff --git a/src/astroapi/__init__.py b/src/astroapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/astroapi/__main__.py b/src/astroapi/__main__.py new file mode 100644 index 0000000..ee313f4 --- /dev/null +++ b/src/astroapi/__main__.py @@ -0,0 +1,5 @@ +from .main import app + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/astroapi/main.py b/src/astroapi/main.py new file mode 100644 index 0000000..170cf9d --- /dev/null +++ b/src/astroapi/main.py @@ -0,0 +1,22 @@ +"""FastAPI application exposing horoscope computation endpoints.""" +from fastapi import FastAPI, HTTPException + +from .models import ChartRequest +from .services import compute_chart + +app = FastAPI(title="AstroCal API") + +@app.get("/health") +def health_check(): + """Simple health check endpoint.""" + return {"status": "ok"} + +@app.post("/chart") +@app.post("/generate_chart") +def generate_chart(request: ChartRequest): + """Compute a Vedic horoscope and return detailed data.""" + try: + return compute_chart(request) + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) + diff --git a/src/astroapi/models.py b/src/astroapi/models.py new file mode 100644 index 0000000..7b7c8c5 --- /dev/null +++ b/src/astroapi/models.py @@ -0,0 +1,14 @@ +from datetime import date, time +from pydantic import BaseModel + +class ChartRequest(BaseModel): + """Parameters required to compute a horoscope chart.""" + name: str + gender: str + birth_date: date + birth_time: time + place: str + latitude: float + longitude: float + timezone: float + diff --git a/src/astroapi/services.py b/src/astroapi/services.py new file mode 100644 index 0000000..9c8516e --- /dev/null +++ b/src/astroapi/services.py @@ -0,0 +1,37 @@ +"""Utility functions for computing horoscope information.""" +from datetime import datetime +from typing import Dict, Any + +from jhora.horoscope import main +from jhora.panchanga import drik + +from .models import ChartRequest + + +def compute_chart(request: ChartRequest) -> Dict[str, Any]: + """Generate horoscope data using existing jhora classes.""" + dob = drik.Date(request.birth_date.year, request.birth_date.month, request.birth_date.day) + tob = (request.birth_time.hour, request.birth_time.minute, request.birth_time.second) + place = drik.Place(request.place, request.latitude, request.longitude, request.timezone) + + horo = main.Horoscope( + place_with_country_code=request.place, + latitude=request.latitude, + longitude=request.longitude, + timezone_offset=request.timezone, + date_in=dob, + birth_time=request.birth_time.strftime("%H:%M:%S"), + ) + + info, charts, houses = horo.get_horoscope_information() + vimsottari = horo._get_vimsottari_dhasa_bhukthi(dob, tob, place) + + return { + "name": request.name, + "gender": request.gender, + "info": info, + "charts": charts, + "ascendant_houses": houses, + "vimsottari_dhasa": vimsottari, + } + diff --git a/src/jhora/ui/horo_chart_tabs.py b/src/jhora/ui/horo_chart_tabs.py index 14bf437..3db2dac 100644 --- a/src/jhora/ui/horo_chart_tabs.py +++ b/src/jhora/ui/horo_chart_tabs.py @@ -18,21 +18,48 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import re, sys, os -sys.path.append('../') -""" Get Package Version from _package_info.py """ -#import importlib.metadata -#_APP_VERSION = importlib.metadata.version('PyJHora') -#---------- -from PyQt6 import QtCore, QtGui -from PyQt6.QtWidgets import QStyledItemDelegate, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTableWidget, \ - QListWidget, QTextEdit, QAbstractItemView, QAbstractScrollArea, QTableWidgetItem, \ - QGridLayout, QLayout, QLabel, QSizePolicy, QLineEdit, QCompleter, QComboBox, \ - QPushButton, QSpinBox, QCheckBox, QApplication, QDoubleSpinBox, QHeaderView, \ - QListWidgetItem,QMessageBox, QFileDialog, QButtonGroup, QRadioButton, QStackedWidget, \ - QTreeWidget -from PyQt6.QtGui import QFont, QFontMetrics +"""PyQt6 based UI for displaying horoscope charts.""" + +import os +import re +import sys + +sys.path.append("../") + +from PyQt6 import QtCore from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont, QFontMetrics +from PyQt6.QtWidgets import ( + QApplication, + QAbstractItemView, + QAbstractScrollArea, + QButtonGroup, + QCheckBox, + QComboBox, + QDoubleSpinBox, + QFileDialog, + QLabel, + QListWidget, + QListWidgetItem, + QHBoxLayout, + QHeaderView, + QLineEdit, + QLayout, + QMessageBox, + QPushButton, + QRadioButton, + QSizePolicy, + QSpinBox, + QStackedWidget, + QStyledItemDelegate, + QTabWidget, + QTableWidget, + QTableWidgetItem, + QTextEdit, + QTreeWidget, + QVBoxLayout, + QWidget, +) from _datetime import datetime, timedelta, timezone import img2pdf from PIL import Image @@ -64,11 +91,14 @@ _images_path = const._IMAGES_PATH _IMAGES_PER_PDF_PAGE = 2 _INPUT_DATA_FILE = const._INPUT_DATA_FILE -_SHOW_GOURI_PANCHANG_OR_SHUBHA_HORA = 0 # 0=Gowri Panchang 1=Shubha Hora +_SHOW_GOURI_PANCHANG_OR_SHUBHA_HORA = 0 # 0=Gowri Panchang 1=Shubha Hora _world_city_csv_file = const._world_city_csv_file -_planet_symbols=const._planet_symbols +_planet_symbols = const._planet_symbols _zodiac_symbols = const._zodiac_symbols -""" UI Constants """ + +# --------------------------------------------------------------------------- +# UI Constants +# --------------------------------------------------------------------------- _main_window_width = 1000#750 #725 _main_window_height = 725#630 #580 # _comp_table_font_size = 8 @@ -247,60 +277,116 @@ _compatibility_tab_start = _prediction_tab_start + 1 _tab_count = len(_tab_names) -available_chart_types = {'south_indian':SouthIndianChart,'north_indian':NorthIndianChart,'east_indian':EastIndianChart, - 'western':WesternChart,'sudarsana_chakra':SudarsanaChakraChart} +available_chart_types = { + 'south_indian': SouthIndianChart, + 'north_indian': NorthIndianChart, + 'east_indian': EastIndianChart, + 'western': WesternChart, + 'sudarsana_chakra': SudarsanaChakraChart, +} available_languages = const.available_languages + +# --------------------------------------------------------------------------- +# Delegate classes +# --------------------------------------------------------------------------- class AlignDelegate(QStyledItemDelegate): + """Center-aligns text within table cells.""" + def initStyleOption(self, option, index): - super(AlignDelegate, self).initStyleOption(option, index) + super().initStyleOption(option, index) option.displayAlignment = QtCore.Qt.AlignmentFlag.AlignHCenter class GrowingTextEdit(QTextEdit): + """Text edit that grows vertically to fit its contents.""" def __init__(self, *args, **kwargs): - super(GrowingTextEdit, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.document().contentsChanged.connect(self.sizeChange) self.heightMin = 0 self.heightMax = 65000 - def sizeChange(self): - docHeight = int(self.document().size().height()) - if self.heightMin <= docHeight <= self.heightMax: - self.setMinimumHeight(docHeight) + def sizeChange(self) -> None: + doc_height = int(self.document().size().height()) + if self.heightMin <= doc_height <= self.heightMax: + self.setMinimumHeight(doc_height) + + +# --------------------------------------------------------------------------- +# Main application widget +# --------------------------------------------------------------------------- class ChartTabbed(QWidget): - def __init__(self,chart_type='south_indian',show_marriage_compatibility=True, calculation_type:str='drik', - language = 'English',date_of_birth=None,time_of_birth=None,place_of_birth=None, gender=0, - use_world_city_database=const.check_database_for_world_cities, - use_internet_for_location_check=const.use_internet_for_location_check): - """ - @param chart_type: One of 'South_Indian','North_Indian','East_Indian','Western','Sudarsana_Chakra' - Default: 'south indian' - @param date_of_birth: string in the format 'yyyy,m,d' e.g. '2024,1,1' or '2024,01,01' - @param time_of_birth: string in the format 'hh:mm:ss' in 24hr format. e.g. '19:07:04' - @param place_of_birth: tuple in the format ('place_name',latitude_float,longitude_float,timezone_hrs_float) - e.g. ('Chennai, India',13.0878,80.2785,5.5) - @param language: One of 'English','Hindi','Tamil','Telugu','Kannada'; Default:English - @param gender: 0='Female',1='Male',2='Transgender',3='No preference'; Default=0 - """ + """Main widget that hosts all horoscope related tabs.""" + + def __init__( + self, + chart_type: str = "south_indian", + show_marriage_compatibility: bool = True, + calculation_type: str = "drik", + language: str = "English", + date_of_birth: str | None = None, + time_of_birth: str | None = None, + place_of_birth: tuple | None = None, + gender: int = 0, + use_world_city_database: bool = const.check_database_for_world_cities, + use_internet_for_location_check: bool = const.use_internet_for_location_check, + ) -> None: + """Initialize the tabbed horoscope widget.""" + super().__init__() + self._initialize_state( + chart_type, + show_marriage_compatibility, + calculation_type, + language, + place_of_birth, + use_world_city_database, + use_internet_for_location_check, + ) + + self._build_ui() + self._populate_defaults(date_of_birth, time_of_birth, gender) + + # ------------------------------------------------------------------ + # Initialization helpers + # ------------------------------------------------------------------ + def _initialize_state( + self, + chart_type: str, + show_marriage_compatibility: bool, + calculation_type: str, + language: str, + place_of_birth, + use_world_city_database: bool, + use_internet_for_location_check: bool, + ) -> None: + """Set up instance variables and global settings.""" + self._horo = None self.use_world_city_database = use_world_city_database self.use_internet_for_location_check = use_internet_for_location_check - self._chart_type = chart_type if chart_type.lower() in const.available_chart_types else 'south_indian' - self._language = language; utils.set_language(available_languages[language]) + self._chart_type = ( + chart_type if chart_type.lower() in const.available_chart_types else "south_indian" + ) + self._language = language + utils.set_language(available_languages[language]) utils.use_database_for_world_cities(use_world_city_database) self.resources = utils.resource_strings self._place_name = place_of_birth self._bhava_chart_type = chart_type self._calculation_type = calculation_type self._show_compatibility = show_marriage_compatibility - ' read world cities' - #self._df = utils._world_city_db_df - #self._world_cities_db = utils.world_cities_db - self._conjunction_dialog_accepted = False; self._conj_planet1=''; self._conj_planet2=''; self._raasi='' - self._lunar_month_type = '' - self._separation_angle_list = [] - self._separation_angle_index = 0 + + self._conjunction_dialog_accepted = False + self._conj_planet1 = "" + self._conj_planet2 = "" + self._raasi = "" + self._lunar_month_type = "" + self._separation_angle_list: list = [] + self._separation_angle_index = 0 + + def _build_ui(self) -> None: + """Construct all UI widgets.""" + self._init_main_window() self._v_layout = QVBoxLayout() self._create_row1_ui() @@ -308,24 +394,31 @@ def __init__(self,chart_type='south_indian',show_marriage_compatibility=True, ca if self._show_compatibility: self._create_comp_ui() self._init_tab_widget_ui() - current_date_str,current_time_str = datetime.now().strftime('%Y,%m,%d;%H:%M:%S').split(';') - if date_of_birth == None: + + def _populate_defaults(self, date_of_birth: str | None, time_of_birth: str | None, gender: int) -> None: + """Populate initial values and compute julian day.""" + + current_date_str, current_time_str = datetime.now().strftime("%Y,%m,%d;%H:%M:%S").split(";") + if date_of_birth is None: self.date_of_birth(current_date_str) - if time_of_birth == None: + if time_of_birth is None: self.time_of_birth(current_time_str) + if not self._validate_ui() and self.use_internet_for_location_check: loc = utils.get_place_from_user_ip_address() - print('loc from IP address',loc) - if len(loc)==4: - print('setting values from loc') - self.place(loc[0],loc[1],loc[2],loc[3]) - if gender==None or gender not in [0,1,2,3]: self.gender(0) - year,month,day = self._dob_text.text().split(",") - dob = (int(year),int(month),int(day)) - tob = tuple([int(x) for x in self._tob_text.text().split(':')]) + if len(loc) == 4: + self.place(loc[0], loc[1], loc[2], loc[3]) + + if gender is None or gender not in [0, 1, 2, 3]: + self.gender(0) + else: + self.gender(gender) + + year, month, day = self._dob_text.text().split(",") + dob = (int(year), int(month), int(day)) + tob = tuple(int(x) for x in self._tob_text.text().split(":")) self._birth_julian_day = utils.julian_day_number(dob, tob) - """ Commented in V4.0.4 to force explicit calling """ - #self.compute_horoscope(calculation_type=self._calculation_type) + def _hide_2nd_row_widgets(self,show=True): self._dob_label.setVisible(show) self._dob_text.setVisible(show)