from datetime import datetime, timezone from django.http import HttpResponse, JsonResponse from timezonefinder import TimezoneFinder import swisseph as swe from django.conf import settings as django_settings from .calc import ( DEFAULT_HOUSE_SYSTEM, PLANET_CODES, SIGNS, calculate_aspects, get_element_counts, get_julian_day, get_planet_positions, jd_to_iso, narrowed_windows, present_signs, set_ephe_path, ) from .models import EphemerisSnapshot def chart(request): dt_str = request.GET.get('dt') lat_str = request.GET.get('lat') lon_str = request.GET.get('lon') if not dt_str or lat_str is None or lon_str is None: return HttpResponse(status=400) try: dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00')) except ValueError: return HttpResponse(status=400) try: lat = float(lat_str) lon = float(lon_str) except ValueError: return HttpResponse(status=400) if not (-90 <= lat <= 90): return HttpResponse(status=400) house_system_param = request.GET.get('house_system') if house_system_param is not None: if not (hasattr(request, 'user') and request.user.is_authenticated and request.user.is_superuser): return HttpResponse(status=403) house_system = house_system_param else: house_system = DEFAULT_HOUSE_SYSTEM set_ephe_path() jd = get_julian_day(dt) planets = get_planet_positions(jd) cusps, ascmc = swe.houses(jd, lat, lon, house_system.encode()) houses = { 'cusps': list(cusps), 'asc': ascmc[0], 'mc': ascmc[1], } return JsonResponse({ 'planets': planets, 'houses': houses, 'elements': get_element_counts(planets), 'aspects': calculate_aspects(planets), 'house_system': house_system, }) def windows(request): """GET /api/windows/ — REVERSE ephemeris lookup (Set the Game Clock). Query params: placements — comma list of Planet:Sign pairs (may be absent/empty) next — planet name; report which signs it can reach in the windows Folds each placement's sign residences into the intersected date windows where ALL placements hold simultaneously, bounded by the game window (settings GAME_WINDOW_START/END — defaults span the precomputed snapshot range: 1781-03-13, Uranus's discovery year, → 2100-12-31). Returns {windows: [{start, end}…], days, next?: {planet, signs: {sign: bool ×12}}} 200 · 400 on an unknown planet/sign or malformed pair. """ placements = {} for pair in [p for p in request.GET.get('placements', '').split(',') if p]: planet, sep, sign = pair.partition(':') if not sep or planet not in PLANET_CODES or sign not in SIGNS: return HttpResponse(status=400) placements[planet] = sign next_planet = request.GET.get('next') or None if next_planet is not None and next_planet not in PLANET_CODES: return HttpResponse(status=400) set_ephe_path() start = getattr(django_settings, 'GAME_WINDOW_START', '1781-03-13') end = getattr(django_settings, 'GAME_WINDOW_END', '2100-12-31') base = [( get_julian_day(datetime.fromisoformat(start).replace(tzinfo=timezone.utc)), get_julian_day(datetime.fromisoformat(end).replace(tzinfo=timezone.utc)), )] wins = narrowed_windows(placements, base) payload = { 'windows': [{'start': jd_to_iso(a), 'end': jd_to_iso(b)} for a, b in wins], 'days': round(sum(b - a for a, b in wins), 2), } if next_planet: reachable = present_signs(next_planet, wins) payload['next'] = { 'planet': next_planet, 'signs': {s: s in reachable for s in SIGNS}, } return JsonResponse(payload) _tf = TimezoneFinder() def timezone_lookup(request): """GET /api/tz/ — resolve IANA timezone string from lat/lon. Query params: lat (float), lon (float) Returns: { "timezone": "America/New_York" } Returns 404 JSON { "timezone": null } if coordinates fall in international waters (no timezone found) — not an error, just no result. """ lat_str = request.GET.get('lat') lon_str = request.GET.get('lon') if lat_str is None or lon_str is None: return HttpResponse(status=400) try: lat = float(lat_str) lon = float(lon_str) except ValueError: return HttpResponse(status=400) if not (-90 <= lat <= 90) or not (-180 <= lon <= 180): return HttpResponse(status=400) tz = _tf.timezone_at(lat=lat, lng=lon) return JsonResponse({'timezone': tz}) def charts_list(request): date_from_str = request.GET.get('date_from') date_to_str = request.GET.get('date_to') if not date_from_str or not date_to_str: return HttpResponse(status=400) try: date_from = datetime.strptime(date_from_str, '%Y-%m-%d').replace( tzinfo=timezone.utc) date_to = datetime.strptime(date_to_str, '%Y-%m-%d').replace( hour=23, minute=59, second=59, tzinfo=timezone.utc) except ValueError: return HttpResponse(status=400) if date_to < date_from: return HttpResponse(status=400) qs = EphemerisSnapshot.objects.filter(dt__gte=date_from, dt__lte=date_to) element_fields = { 'fire_min': 'fire', 'water_min': 'water', 'earth_min': 'earth', 'air_min': 'air', 'time_min': 'time_el', 'space_min': 'space_el', } for param, field in element_fields.items(): value = request.GET.get(param) if value is not None: try: qs = qs.filter(**{f'{field}__gte': int(value)}) except ValueError: return HttpResponse(status=400) results = [ { 'dt': snap.dt.isoformat(), 'elements': snap.elements_dict(), 'planets': snap.chart_data.get('planets', {}), } for snap in qs ] return JsonResponse({'results': results, 'count': len(results)})