Ë
    }D™j;\  ã                   óh  — U d Z ddlmZmZmZmZmZmZmZm	Z	 ddl
mZmZmZmZmZ ddlmZmZmZmZ ddlmZmZmZmZmZmZmZ ddlZddlZddlmZ  ee «      Z!i dd“d	d
“dd“dd“dd“dd“dd“dd“dd“dd“dd“dd“dd “d!d"“d#d$“d%d&“d'd(“d)d*d+d,œ¥Z"e#e$e%f   e&d-<    ed.d/d0d1d¬2«       ed3d4d5d6d
¬2«       ed7d8d9d:d¬2«       ed;d/d<d=d¬2«       ed>d4d0d?d¬2«       ed@d8d5dAd¬2«       edBd/d9dCd¬2«       edDd4d<dEd¬2«       edFd8d0dGd¬2«       edHd/d5dId¬2«       edJd4d9dKd¬2«       edLd8d<dMd¬2«      dNœZ'e#e%ef   e&dO<   dPdQdRdSdTdUdVdWdXdYdZd[d\œZ(e#e%ef   e&d]<   e(jS                  «       D � �ci c]  \  } }|| “Œ
 c}} Z*e#ee%f   e&d^<   d_e$d`dfda„Z+dbe$d`e	fdc„Z,dded`e%fde„Z-	 	 	 d“dfee%e.f   ddeeef   dgedhee.   diee.   djee.   d`efdk„Z/dlee%e.f   dmee%e.f   dnee%e.f   d`e0fdo„Z1dpee%e.f   dqe2d`efdr„Z3dse%d`efdt„Z4dued`e%fdv„Z5dweeeef   d`e2e   fdx„Z6dwed`e2e   fdy„Z7dze2e   d{e2e   d`e2e   fd|„Z8d}e%d`e%fd~„Z9d}e%d`efd„Z:d}e%d`efd€„Z;d�e.d`e.fd‚„Z<dƒee%e.f   d„ee%e.f   d`e.fd…„Z=d†e2ee%e.f      d`e2ee%e.f      fd‡„Z>dˆed`e.fd‰„Z?dŠe.d`efd‹„Z@dŒe$d`e$fd�„ZAdŽe#e$e.f   d`e#e$e%f   fd�„ZBd�e.d‘e.d`efd’„ZCyc c}} w )”a˜  
Kerykeion Utilities Module

This module provides utility functions for astrological calculations including:
- Zodiac position conversions and validations
- House position determinations
- Lunar phase calculations
- Angular mathematics (circular mean, sorting)
- Date/time conversions (Julian Day)
- SVG processing utilities

Author: Giacomo Battaglia
Copyright: (C) 2025 Kerykeion Project
License: AGPL-3.0
é    )ÚKerykeionPointModelÚKerykeionExceptionÚZodiacSignModelÚAstrologicalSubjectModelÚLunarPhaseModelÚCompositeSubjectModelÚPlanetReturnModelÚ
ZodiacType)ÚLunarPhaseEmojiÚLunarPhaseNameÚ	PointTypeÚAstrologicalPointÚHouses)ÚUnionÚOptionalÚget_argsÚcast)ÚDEBUGÚINFOÚWARNINGÚERRORÚCRITICALÚbasicConfigÚ	getLoggerN)ÚdatetimeÚSunÚMooné   ÚMercuryé   ÚVenusé   ÚMarsé   ÚJupiteré   ÚSaturné   ÚUranusé   ÚNeptuneé   ÚPlutoé	   ÚMean_North_Lunar_Nodeé
   ÚTrue_North_Lunar_Nodeé   ÚMean_South_Lunar_Nodeiè  ÚTrue_South_Lunar_NodeiL  ÚChironé   ÚMean_Lilithé   Ú	Ascendanti¬&  i­&  i®&  i¯&  )Ú
DescendantÚMedium_CoeliÚ
Imum_CoeliÚ_POINT_NUMBER_MAPÚAriÚCardinalÚFireu   â™ˆï¸�)ÚsignÚqualityÚelementÚemojiÚsign_numÚTauÚFixedÚEarthu   â™‰ï¸�ÚGemÚMutableÚAiru   â™Šï¸�ÚCanÚWateru   â™‹ï¸�ÚLeou   â™Œï¸�ÚViru   â™�ï¸�ÚLibu   â™Žï¸�ÚScou   â™�ï¸�ÚSagu   â™�ï¸�ÚCapu   â™‘ï¸�ÚAquu   â™’ï¸�ÚPisu   â™“ï¸�)r   r   r    r"   r$   r&   r(   r*   r,   r.   r0   r2   Ú_ZODIAC_SIGNSÚFirst_HouseÚSecond_HouseÚThird_HouseÚFourth_HouseÚFifth_HouseÚSixth_HouseÚSeventh_HouseÚEighth_HouseÚNinth_HouseÚTenth_HouseÚEleventh_HouseÚTwelfth_House)r   r    r"   r$   r&   r(   r*   r,   r.   r0   r2   r8   Ú_HOUSE_NAMESÚ_HOUSE_NUMBERSÚlevelÚreturnc                 óð   — | xs dj                  «       j                  «       }t        t        t        t
        t        dœ}|j                  |t        «      }t        d|¬«       t        j                  |«       y)zê
    Configure the root logger for consistent formatting across the library.

    Args:
        level: Logging level as string (debug, info, warning, error, critical).
               Case-insensitive. Defaults to INFO if invalid.
    Ú )ÚdebugÚinfoÚwarningÚerrorÚcriticalz4%(asctime)s - %(name)s - %(levelname)s - %(message)s)Úformatre   N)ÚstripÚlowerr   r   r   r   r   Úgetr   ÚloggerÚsetLevel)re   Únormalized_levelÚ	level_mapÚselected_levels       ú_/var/www/html/Astrologybackend/backend/venv/lib/python3.12/site-packages/kerykeion/utilities.pyÚsetup_loggingrx   q   se   € ð š ×*Ñ*Ó,×2Ñ2Ó4ÐäÜÜÜÜñ!€Ið —]‘]Ð#3´TÓ:€NÜØEØõô ‡O�O�NÕ#ó    Úvaluec                 óª   — | j                  «       }|dv rt        t        d«      S |dk(  rt        t        d«      S t        dj	                  | ¬«      «      ‚)a©  
    Normalize a zodiac type string to its canonical representation.

    Handles case-insensitive matching and legacy formats like "tropic" or "Tropic",
    automatically converting them to the canonical forms "Tropical" or "Sidereal".

    Args:
        value: Input zodiac type string (case-insensitive).

    Returns:
        ZodiacType: Canonical zodiac type ("Tropical" or "Sidereal").

    Raises:
        ValueError: If `value` is not a recognized zodiac type.

    Examples:
        >>> normalize_zodiac_type("tropical")
        'Tropical'
        >>> normalize_zodiac_type("Tropic")
        'Tropical'
        >>> normalize_zodiac_type("SIDEREAL")
        'Sidereal'
    )ÚtropicalÚtropicÚTropicalÚsiderealÚSiderealzƒ'{value}' is not a valid zodiac type. Accepted values are: Tropical, Sidereal (case-insensitive, 'tropic' also accepted as legacy).©rz   )rp   r   r
   Ú
ValueErrorrn   )rz   Úvalue_lowers     rw   Únormalize_zodiac_typer„   �   sZ   € ð0 —+‘+“-€KàÐ,Ñ,Ü”J 
Ó+Ð+Ø	˜
Ò	"Ü”J 
Ó+Ð+äðDßDJÁFÐQVÀFÓDWó
ð 	
ry   Únamec                 óh   — 	 t         t        | «         S # t        $ r}t        d| › �«      |‚d}~ww xY w)a.  
    Convert an astrological point name to its corresponding numerical identifier.

    Args:
        name: The name of the astrological point

    Returns:
        The numerical identifier used in Swiss Ephemeris calculations

    Raises:
        KerykeionException: If the name is not recognized
    z)Error in getting number from name! Name: N)r=   ÚstrÚKeyErrorr   )r…   Úexcs     rw   Úget_number_from_namerŠ   ´   s?   € ð^Ü ¤ T£Ñ+Ð+øÜò ^Ü Ð#LÈTÈFÐ!SÓTÐZ]Ð]ûð^ús   ‚ ”	1�,¬1ÚdegreeÚ
point_typeÚspeedÚdeclinationÚ	magnitudec                 ó  — | dk  r| dz  } | dk\  rt        d| › �«      ‚t        | dz  «      }| dz  }t        |   }t        ||j                  |j
                  |j                  |j                  || |j                  ||||¬«      S )a¼  
    Create a KerykeionPointModel from a degree position.

    Args:
        degree: The degree position (0-360, negative values are converted to positive)
        name: The name of the celestial point or house
        point_type: The type classification of the point
        speed: The velocity/speed of the celestial point in degrees per day (optional)
        declination: The declination of the celestial point in degrees (optional)
        magnitude: The apparent visual magnitude for fixed stars (optional)

    Returns:
        A KerykeionPointModel with calculated zodiac sign, position, and properties

    Raises:
        KerykeionException: If the degree is >= 360 after normalization
    r   éh  z)Error in calculating positions! Degrees: é   )r…   rB   rC   rA   rE   ÚpositionÚabs_posrD   rŒ   r�   rŽ   r�   )	r   ÚintrV   r   rB   rC   rA   rE   rD   )	r‹   r…   rŒ   r�   rŽ   r�   Ú
sign_indexÚsign_degreeÚzodiac_signs	            rw   Úget_kerykeion_point_from_degreer™   Ç   sž   € ð4 �‚zØ˜#‘ˆà�‚}Ü Ð#LÈVÈHÐ!UÓVÐVä�V˜r‘\Ó"€JØ˜2‘+€KÜ 
Ñ+€KäØØ×#Ñ#Ø×#Ñ#Ø×ÑØ×%Ñ%ØØØ×ÑØØØØôð ry   Ústart_angleÚ	end_angleÚ	candidatec                 óð   — d„ } || «      } ||«      } ||«      }||z
  dz  }|dkD  rt        d|› �«      ‚t        j                  ||dd¬«      ryt        j                  ||dd¬«      ry	||z
  dz  }||k  S )
ur  
    Check if a candidate angle lies on the clockwise arc from start to end angle.

    Args:
        start_angle: Starting angle in degrees
        end_angle: Ending angle in degrees
        candidate: Angle to check

    Returns:
        True if candidate is on the clockwise arc from start to end

    Raises:
        KerykeionException: If the arc exceeds 180Â°
    c                 ó   — | dz  S )Nr‘   © r�   s    rw   ú<lambda>z"is_point_between.<locals>.<lambda>  s
   € ˜e c™k€ ry   r‘   é´   uN   The angle between start and end point is not allowed to exceed 180Â°, yet is: g•Ö&è.>gê-�™—q=)Úrel_tolÚabs_tolTF)r   ÚmathÚisclose)	rš   r›   rœ   Ú	normalizeÚstartÚendÚtargetÚspanÚdistance_from_starts	            rw   Úis_point_betweenr¬      s•   € ñ" *€Iá�kÓ"€EÙ
�IÓ
€CÙ�yÓ!€FØ�%‰K˜3Ñ€Dàˆc‚zÜ Ð#qÐrvÐqwÐ!xÓyÐyÜ‡|�|�F˜E¨4¸Õ?ØÜ‡|�|�F˜C¨°uÕ=ØØ! E™>¨SÑ0ÐØ Ñ%Ð%ry   Úplanet_degreeÚhouses_degree_ut_listc                 óÎ   — t        t        «      }t        t        |«      «      D ]/  }||   }||dz   t        |«      z     }t	        ||| «      sŒ*||   c S  t        d| › d|› �«      ‚)am  
    Determine which house contains a planet based on its degree position.

    Args:
        planet_degree: The planet's position in degrees (0-360)
        houses_degree_ut_list: List of house cusp degrees

    Returns:
        The house name containing the planet

    Raises:
        ValueError: If the planet's position doesn't fall within any house range
    r   z$Error in house calculation, planet: z
, houses: )r   r   ÚrangeÚlenr¬   r‚   )r­   r®   Úhouse_namesÚiÚstart_degreeÚ
end_degrees         rw   Úget_planet_houser¶   "  s}   € ô œ6Ó"€Kä”3�{Ó#Ó$ò "ˆØ,¨QÑ/ˆØ*¨A°©E´SÐ9NÓ5OÑ+OÑPˆ
ä˜L¨*°mÕDØ˜q‘>Ò!ð"ô Ð;¸M¸?È*ÐUjÐTkÐlÓ
mÐmry   Úhouse_numberc                 óR   — t         j                  | d«      }|€t        d| › �«      ‚|S )zá
    Convert a house number to its corresponding house name.

    Args:
        house_number: House number (1-12)

    Returns:
        The house name

    Raises:
        ValueError: If house_number is not in range 1-12
    NzInvalid house number: )rc   rq   r‚   )r·   r…   s     rw   Úget_house_namer¹   <  s3   € ô ×Ñ˜L¨$Ó/€DØ€|ÜÐ1°,°Ð@ÓAÐAØ€Kry   Ú
house_namec                 óR   — t         j                  | d«      }|€t        d| › �«      ‚|S )zÚ
    Convert a house name to its corresponding house number.

    Args:
        house_name: The house name

    Returns:
        House number (1-12)

    Raises:
        ValueError: If house_name is not recognized
    NzInvalid house name: )rd   rq   r‚   )rº   Únumbers     rw   Úget_house_numberr½   O  s3   € ô ×Ñ 
¨DÓ1€FØ€~ÜÐ/°
¨|Ð<Ó=Ð=Ø€Mry   Úsubjectc                 óp   — g }| j                   D ]$  }|j                  | |j                  «          «       Œ& |S )zÝ
    Get a list of house objects in order from the subject.

    Args:
        subject: The astrological subject containing house data

    Returns:
        List of KerykeionPointModel objects representing the houses
    )Úhouses_names_listÚappendrp   )r¾   Úhouses_absolute_position_listÚhouses      rw   Úget_houses_listrÄ   b  sB   € ð %'Ð!Ø×*Ñ*ò EˆØ%×,Ñ,¨W°U·[±[³]Ñ-CÕDðEð )Ð(ry   c                 óp   — g }| j                   D ]$  }|j                  | |j                  «          «       Œ& |S )zæ
    Get a list of active astrological point objects from the subject.

    Args:
        subject: The astrological subject containing point data

    Returns:
        List of KerykeionPointModel objects for all active points
    )Úactive_pointsrÁ   rp   )r¾   Úplanets_absolute_position_listÚplanets      rw   Ú&get_available_astrological_points_listrÉ   u  sB   € ð &(Ð"Ø×'Ñ'ò GˆØ&×-Ñ-¨g°f·l±l³nÑ.EÕFðGð *Ð)ry   Úfirst_pointsÚsecond_pointsc                 óB   — t        t        | «      t        |«      z  «      S )a  
    Find astrological points that appear in both input lists.

    Args:
        first_points: First list of astrological points
        second_points: Second list of astrological points

    Returns:
        List of points common to both input lists (without duplicates)
    )ÚsortedÚset)rÊ   rË   s     rw   Úfind_common_active_pointsrÏ   †  s   € ô ”#�lÓ#¤c¨-Ó&8Ñ8Ó9Ð9ry   Úphasec                 óš   — | dk(  ry| dk  ryd| cxk  rdk  ry | dk  ry| dk(  ry| d	k  ry
d	| cxk  rdk  ry | dk  ryt        d| › �«      ‚)a  
    Get the index for lunar phase lookup based on phase number.

    Args:
        phase: The lunar phase number (1-28)

    Returns:
        Index (0-7) for lunar phase lookup arrays

    Raises:
        KerykeionException: If phase is outside valid range
    r   r   r*   r.   r    é   r"   r$   é   r&   é   r(   é   z)Error in lunar phase calculation! Phase: )r   )rÐ   s    rw   Ú_get_lunar_phase_indexrÖ   ›  s{   € ð �‚zØØ	�ŠØØ	
ˆeŒ�qŠØð 
à	�ŠØØ	�"ŠØØ	�ŠØØ	ˆuÔ	˜Ò	Øð 
à	�"ŠØä Ð#LÈUÈGÐ!TÓUÐUry   c                 ó@   — t        t        «      }t        | «      }||   S )zû
    Get the emoji representation of a lunar phase.

    Args:
        phase: The lunar phase number (0-28)

    Returns:
        The corresponding emoji for the lunar phase

    Raises:
        KerykeionException: If phase is outside valid range
    )r   r   rÖ   )rÐ   Úlunar_phase_emojisÚindexs      rw   Úget_moon_emoji_from_phase_intrÚ   ¼  s$   € ô "¤/Ó2ÐÜ" 5Ó)€EØ˜eÑ$Ð$ry   c                 ó@   — t        t        «      }t        | «      }||   S )a  
    Get the name of a lunar phase from its numerical value.

    Args:
        phase: The lunar phase number (0-28)

    Returns:
        The corresponding name for the lunar phase

    Raises:
        KerykeionException: If phase is outside valid range
    )r   r   rÖ   )rÐ   Úlunar_phase_namesrÙ   s      rw   Ú"get_moon_phase_name_from_phase_intrÝ   Î  s$   € ô !¤Ó0ÐÜ" 5Ó)€EØ˜UÑ#Ð#ry   Úlatitudec                 óz   — | dkD  rd} t         j                  d«       | S | dk  rd} t         j                  d«       | S )u'  
    Adjust latitude values for polar regions to prevent calculation errors.

    Latitudes beyond Â±66Â° are clamped to Â±66Â° for house calculations.

    Args:
        latitude: The original latitude value

    Returns:
        The adjusted latitude value, clamped between -66Â° and 66Â°
    g     €P@u:   Latitude capped at 66Â° to keep house calculations stable.g     €PÀu;   Latitude capped at -66Â° to keep house calculations stable.)rr   rj   )rÞ   s    rw   Úcheck_and_adjust_polar_latituderà   à  sE   € ð �$‚ØˆÜ�‰ÐPÔQð €Oð	 
�EÒ	ØˆÜ�‰ÐQÔRà€Ory   Úfirst_positionÚsecond_positionc                 ó¼  — t        j                  t        j                  | «      «      t        j                  t        j                  |«      «      z   dz  }t        j                  t        j                  | «      «      t        j                  t        j                  |«      «      z   dz  }t        j                  t        j
                  ||«      «      }|dk  r|dz  }|S )u¡  
    Calculate the circular mean of two angular positions.

    This method correctly handles positions that cross the 0Â°/360Â° boundary,
    avoiding errors that occur with simple arithmetic means.

    Args:
        first_position: First angular position in degrees (0-360)
        second_position: Second angular position in degrees (0-360)

    Returns:
        The circular mean position in degrees (0-360)
    r    r   r‘   )r¤   ÚcosÚradiansÚsinÚdegreesÚatan2)rá   râ   ÚxÚyÚmean_positions        rw   Úcircular_meanrì   ü  s�   € ô 
�‰”$—,‘,˜~Ó.Ó	/´$·(±(¼4¿<¹<ÈÓ;XÓ2YÑ	YÐ]^Ñ^€AÜ	�‰”$—,‘,˜~Ó.Ó	/´$·(±(¼4¿<¹<ÈÓ;XÓ2YÑ	YÐ]^Ñ^€AÜ—L‘L¤§¡¨A¨qÓ!1Ó2€Mà�qÒØ˜ÑˆàÐry   rç   c                 ó|  ‡— | st        d«      ‚t        d„ | D «       «      s6t        d„ | D «       «      }t        d|› dt        |«      j                  › �«      ‚t        | «      dk  r| j                  «       S | d   Šdt        t        t        f   d	t        t        t        f   fˆfd
„}| dd }t        ||¬«      }‰g|z   S )a7  
    Sort degrees in circular clockwise progression starting from the first element.

    Args:
        degrees: List of numeric degree values

    Returns:
        List sorted by clockwise distance from the first element

    Raises:
        ValueError: If the list is empty or contains non-numeric values
    zInput list cannot be emptyc              3   óH   K  — | ]  }t        |t        t        f«      –— Œ y ­w©N©Ú
isinstancer•   Úfloat)Ú.0r‹   s     rw   ú	<genexpr>z circular_sort.<locals>.<genexpr>$  s   è ø€ ÒF°FŒz˜&¤3¬ ,×/ÑFùs   ‚ "c              3   óN   K  — | ]  }t        |t        t        f«      rŒ|–— Œ y ­wrï   rð   )ró   Úds     rw   rô   z circular_sort.<locals>.<genexpr>%  s   è ø€ ÒM˜Q´¸AÄÄU¸|Õ1L”qÑMùs   ‚%ž%z%All elements must be numeric, found: z	 of type r   r   Úanglerf   c                 ó:   •— ‰dz  }| dz  }||z
  }|dk  r|dz  }|S )Nr‘   r   rŸ   )r÷   Úref_normÚ
angle_normÚdistanceÚ	references       €rw   Úclockwise_distancez)circular_sort.<locals>.clockwise_distance-  s4   ø€ Ø˜s‘?ˆØ˜S‘[ˆ
Ø Ñ(ˆØ�aŠ<Ø˜‰OˆHØˆry   N)Úkey)r‚   ÚallÚnextÚtypeÚ__name__r±   Úcopyr   r•   rò   rÍ   )rç   Úinvalidrý   Ú	remainingÚsorted_remainingrü   s        @rw   Úcircular_sortr    sÈ   ø€ ñ ÜÐ5Ó6Ð6äÑF¸gÔFÔFÜÑM 'ÔMÓMˆÜÐ@ÀÀ	ÈÔSWÐX_ÓS`×SiÑSiÐRjÐkÓlÐlä
ˆ7ƒ|�qÒØ�|‰|‹~Ðà˜‘
€Ið¤%¬¬U¨
Ñ"3ð ¼¼cÄ5¸jÑ8Iõ ð ˜˜�€IÜ˜iÐ-?Ô@Ðàˆ;Ð)Ñ)Ð)ry   Údtc                 óx  — | j                   }| j                  }| j                  }|dk  r
|dz  }|dz  }|dz  }d|z
  |dz  z   }t        d|dz   z  «      t        d|dz   z  «      z   |z   |z   d	z
  }| j                  }| j
                  }| j                  }	| j                  }
|||d
z  z   |	dz  z   |
dz  z   dz  z  }|S )zÀ
    Convert a Python datetime object to Julian Day Number.

    Args:
        dt: The datetime object to convert

    Returns:
        The corresponding Julian Day Number (JD) as a float
    r    r   r8   éd   r$   ç     Ôv@él  çaTR' ™>@g     Ò—@é<   i  l    $'- é   )ÚyearÚmonthÚdayr•   ÚhourÚminuteÚsecondÚmicrosecond)r  r  r  r  ÚaÚbÚjdr  r  r  r  s              rw   Údatetime_to_julianr  @  sÜ   € ð �7‰7€DØ�H‰H€EØ
�&‰&€Cà�‚zØ�‰	ˆØ�‰ˆà�‰€AØ	ˆA‰��a‘Ñ€Aä	ˆV�t˜d‘{Ñ#Ó	$¤s¨7°e¸a±iÑ+@Ó'AÑ	AÀCÑ	GÈ!Ñ	KÈfÑ	T€Bà�7‰7€DØ�Y‰Y€FØ�Y‰Y€FØ—.‘.€Kàˆ4�&˜2‘+Ñ ¨¡Ñ-°¸jÑ0HÑHÈBÑ
NÑN€Bà€Iry   r  c           	      ój  — | dz   }t        |«      }||z
  }|dk  r|}n(t        |dz
  dz  «      }|dz   |z   t        |dz  «      z
  }|dz   }t        |dz
  d	z  «      }t        d	|z  «      }t        ||z
  d
z  «      }	||z
  t        d
|	z  «      z
  |z   }
t        |
«      }|
|z
  }t        |dz  «      }t        |dz  |z
  dz  «      }t        |dz  dz  |dz  z
  |z
  dz  «      }t        |dz  dz  |dz  z
  |z
  dz  |z
  dz  «      }|	dk  r|	dz
  }n|	dz
  }|dkD  r|dz
  }n|dz
  }t        |||||||«      S )z®
    Convert a Julian Day Number to a Python datetime object.

    Args:
        jd: Julian Day Number as a float

    Returns:
        The corresponding datetime object
    g      à?i# g   @Ð}<Ag    ˆÕá@r   r$   iô  gfffff†^@r  r  r  r  i@B rÒ   é   r    r  ik  )r•   r   )r  Újd_plusÚZÚFÚAÚalphaÚBÚCÚDÚEr  Úday_intÚday_fracÚhoursÚminutesÚsecondsÚmicrosecondsr  r  s                      rw   Újulian_to_datetimer,  a  sˆ  € ð �3‰h€GäˆG‹€AØ�!‰€Aàˆ7‚{Ø‰ä�Q˜‘^ xÑ/Ó0ˆØ�‰E�E‰MœC ¨¡	›NÑ*ˆà	ˆD‰€AÜˆQ�‰Y˜&Ñ Ó!€AÜˆF�Q‰J‹€AÜˆQ�‰U�gÑÓ€Aà
ˆa‰%”#�g ‘kÓ"Ñ
" QÑ
&€CÜ�#‹h€Gà�W‰}€HÜ�˜2‘Ó€EÜ�8˜b‘= 5Ñ(¨BÑ.Ó/€GÜ�8˜b‘= 2Ñ%¨°©
Ñ2°WÑ<ÀÑBÓC€GÜ˜ B™¨Ñ+¨e°b©jÑ8¸7ÑBÀbÑHÈ7ÑRÐV]Ñ]Ó^€Làˆ2‚vØ�A‘‰à�B‘ˆàˆq‚yØ�4‰x‰à�4‰xˆä�D˜% ¨%°¸'À<ÓPÐPry   Úsvg_contentc                 ó  ‡‡— i Št        j                  dt         j                  «      }|j                  | «      }|D ]e  }t        j                  d«      }|j	                  |«      D ]:  }|j                  d«      }|j                  d«      j                  «       }|‰d|› �<   Œ< Œg |j                  d| «      }ˆfd„Št        j                  d«      }	|}
|	j                  |
«      r'|	j                  ˆfd	„|
«      }
|	j                  |
«      rŒ'|
S )
aƒ  
    Replace CSS custom properties (variables) with their values in SVG content.

    Extracts CSS variables from style blocks, replaces var() references with actual values,
    and removes all style blocks from the SVG.

    Args:
        svg_content: The original SVG string with CSS variables

    Returns:
        Modified SVG with CSS variables inlined and style blocks removed
    z<style.*?>(.*?)</style>z!--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);r   r    z--rh   c                 óÂ   •— | j                  d«      j                  «       }| j                  d«      r| j                  d«      nd }|‰v r‰|   S |r|j                  «       S y)Nr   r"   rh   )Úgroupro   )ÚmatchÚvariable_nameÚfallback_valueÚcss_variable_maps      €rw   Úreplace_css_variable_referencezCinline_css_variables_in_svg.<locals>.replace_css_variable_reference°  s[   ø€ ØŸ™ A›×,Ñ,Ó.ˆØ+0¯;©;°q¬>˜Ÿ™ Qœ¸tˆàÐ,Ñ,Ø# MÑ2Ð2ÙØ!×'Ñ'Ó)Ð)àry   z(var\(\s*(--[\w-]+)\s*(,\s*([^)]+))?\s*\)c                 ó   •—  ‰| «      S rï   rŸ   )Úmr5  s    €rw   r    z-inline_css_variables_in_svg.<locals>.<lambda>¿  s   ø€ Ñ=[Ð\]Ó=^€ ry   )	ÚreÚcompileÚDOTALLÚfindallÚfinditerr0  ro   ÚsubÚsearch)r-  Ústyle_tag_patternÚstyle_blocksÚstyle_blockÚcss_variable_patternr1  r2  Úvariable_valueÚsvg_without_style_blocksÚvariable_usage_patternÚprocessed_svgr4  r5  s              @@rw   Úinline_css_variables_in_svgrG  –  s  ù€ ð ÐÜŸ
™
Ð#=¼r¿y¹yÓIÐØ$×,Ñ,¨[Ó9€Là#ò DˆÜ!Ÿz™zÐ*NÓOÐØ)×2Ñ2°;Ó?ò 	DˆEØ!ŸK™K¨›NˆMØ"Ÿ[™[¨›^×1Ñ1Ó3ˆNØ5CÐ˜r - Ð1Ò2ñ	DðDð  1×4Ñ4°R¸ÓEÐô	ô  ŸZ™ZÐ(SÓTÐà,€MØ
 ×
'Ñ
'¨Ô
6Ø.×2Ñ2Ó3^Ð`mÓnˆð !×
'Ñ
'¨Õ
6ð Ðry   Úvaluesc                 óª  — | si S t        | j                  «       «      }|dk(  r| j                  «       D �ci c]  }|d“Œ c}S | j                  «       D ��ci c]  \  }}||dz  |z  “Œ }}}|j                  «       D ��ci c]  \  }}|t	        |«      “Œ }}}|j                  «       D �ci c]  }|||   ||   z
  “Œ }}t        |j                  «       «      }d|z
  }t        |j                  «       d„ d¬«      }	|j                  «       }
t        |«      D ]&  }|t        |	«      k  sŒ|	|   d   }|
|xx   dz  cc<   Œ( |
S c c}w c c}}w c c}}w c c}w )ac  
    Distribute percentages so they sum to exactly 100.

    This function uses a largest remainder method to ensure that
    the percentage total equals 100 even after rounding.

    Args:
        values: Dictionary with keys and their raw percentage values

    Returns:
        Dictionary with the same keys and integer percentages that sum to 100
    r   r
  c                 ó   — | d   S )Nr   rŸ   )ré   s    rw   r    z/distribute_percentages_to_100.<locals>.<lambda>ä  s
   € À1ÀQÁ4€ ry   T)rþ   Úreverser   )	ÚsumrH  ÚkeysÚitemsr•   rÍ   r  r°   r±   )rH  Útotalrþ   rz   ÚpercentagesÚinteger_partsÚ
remaindersÚcurrent_sumÚneededÚsorted_by_remainderÚresultr³   s               rw   Údistribute_percentages_to_100rW  É  sX  € ñ Øˆ	ä�—‘“Ó €EØ�‚zØ"(§+¡+£-Ö0˜3��Q‘Ò0Ð0à>D¿l¹l»n×M±
°°U�3˜ ™ eÑ+Ñ+ÐM€KÑMØ7B×7HÑ7HÓ7J×K©¨¨e�Sœ#˜e›*‘_ÐK€MÑKØHS×HXÑHXÓHZÖ[À�#�{ 3Ñ'¨-¸Ñ*<Ñ<Ñ<Ð[€JÐ[ä�m×*Ñ*Ó,Ó-€KØ�;Ñ€Fä  ×!1Ñ!1Ó!3¹ÐQUÔVÐà×ÑÓ!€FÜ�6‹]ò ˆØŒsÐ&Ó'Ó'Ø% aÑ(¨Ñ+ˆCØ�3‹K˜1ÑŒKðð
 €Mùò# 1ùãMùÛKùÚ[s   µ
D?ÁEÁ>E
Â)EÚmoon_abs_posÚsun_abs_posc                 óz   — | |z
  dz  }d}t        ||z  «      dz   }t        ||t        |«      t        |«      ¬«      S )a  
    Calculate lunar phase information from Sun and Moon positions.

    Args:
        moon_abs_pos: Absolute position of the Moon in degrees
        sun_abs_pos: Absolute position of the Sun in degrees

    Returns:
        LunarPhaseModel containing phase data, emoji, and name
    r‘   gnÛ¶mÛ¶)@r   )Údegrees_between_s_mÚ
moon_phaseÚ
moon_emojiÚmoon_phase_name)r•   r   rÚ   rÝ   )rX  rY  Údegrees_betweenÚstepr\  s        rw   Úcalculate_moon_phasera  ï  sQ   € ð $ kÑ1°SÑ8€Oð €DÜ�_¨Ñ,Ó-°Ñ1€JäØ+ØÜ0°Ó<Ü:¸:ÓFô	ð ry   )NNN)DÚ__doc__Úkerykeion.schemasr   r   r   r   r   r   r	   r
   Úkerykeion.schemas.kr_literalsr   r   r   r   r   Útypingr   r   r   r   Úloggingr   r   r   r   r   r   r   r¤   r8  r   r  rr   r=   Údictr‡   r•   Ú__annotations__rV   rc   rN  rd   rx   r„   rŠ   rò   r™   Úboolr¬   Úlistr¶   r¹   r½   rÄ   rÉ   rÏ   rÖ   rÚ   rÝ   rà   rì   r  r  r,  rG  rW  ra  )ÚkÚvs   00rw   ú<module>rm     sw  ðò÷ 	÷ 	ó 	÷õ ÷ 3Ó 2ß Q× QÑ QÛ Û 	Ý ñ 
�8Ó	€ð%Ø	ˆ1ð%à
ˆAð%ð ˆqð%ð ˆQð	%ð
 ˆAð%ð ˆqð%ð ˆað%ð ˆað%ð ˆqð%ð ˆQð%ð ˜Rð%ð ˜Rð%ð ˜Tð%ð ˜Tð%ð  ˆbð!%ð" �2ð#%ð$ �ð%%ð& ØØò+%Ð �4˜˜S˜‘>ó ñ4 ˜E¨:¸vÈXÐ`aÔbÙ˜E¨7¸GÈ8Ð^_Ô`Ù˜E¨9¸eÈ8Ð^_Ô`Ù˜E¨:¸wÈhÐabÔcÙ˜E¨7¸FÈ(Ð]^Ô_Ù˜E¨9¸gÈXÐ`aÔbÙ˜E¨:¸uÈHÐ_`ÔaÙ˜E¨7¸GÈ8Ð^_Ô`Ù˜E¨9¸fÈHÐ_`ÔaÙ˜E¨:¸wÈhÐabÔcÙ˜U¨G¸UÈ(Ð]_Ô`Ù˜U¨I¸wÈhÐacÔdñ-€ˆt�C˜Ð(Ñ)ó ð" ØØØØØØØØØØØñ#€ˆd�3˜�;Ñó ð 7C×6HÑ6HÓ6J×$K©d¨a° Q¨¡TÓ$K€��V˜S�[Ñ!Ó Kð$˜ð $ ó $ð<"
 ð "
¨ó "
ðJ^Ð0ð ^°Só ^ð. "Ø#'Ø!%ñ1Ø�#�u�*Ñð1à
Ð! 6Ð)Ñ
*ð1ð ð1ð �E‰?ð	1ð
 ˜%‘ð1ð ˜‰ð1ð ó1ðr&Ø�s˜E�zÑ"ð&Ø/4°S¸%°ZÑ/@ð&ØMRÐSVÐX]ÐS]ÑM^ð&à	ó&ðDn E¨#¨u¨*Ñ$5ð nÈdð nÐW]ó nð4 ð ¨ó ð& ð ¨Có ð&)ØÐ+Ð-BÐDUÐUÑVð)à	Ð
Ñó)ð&*Ð4Lð *ÐQUÐViÑQjó *ð":ØÐ(Ñ)ð:Ø:>Ð?PÑ:Qð:à	Ð
Ñó:ð*V #ð V¨#ó VðB%¨ð %°ó %ð$$¨cð $°nó $ð$¨eð ¸ó ð8 %¨¨U¨
Ñ"3ð ÀeÈCÐQVÈJÑFWð Ð\aó ð0$*˜4  c¨5 jÑ 1Ñ2ð $*°t¸EÀ#ÀuÀ*Ñ<MÑ7Nó $*ðX˜8ð ¨ó ðB-Q˜5ð -Q Xó -Qðj+¨Sð +°Só +ðf#¨$¨s°E¨zÑ*:ð #¸tÀCÈÀH¹~ó #ðL uð ¸5ð À_ô ùóM %Ls   Å(J.