r"""Routines for calculation and of solar and sidereal times.

This module can:

- Convert between Python :class:`~datetime.datetime`, UNIX times (as floats),
  and Skyfield time objects.
- Determine the number of leap seconds in an interval (thanks Skyfield!)
- Calculate the Earth Rotation Angle (the successor to Sidereal Time)
- Determine various local time standards (Local Stellar Angle and Day)
- Generate a nice wrapper for Skyfield to ease the loading of required data.

Time Utilities
==============
Time conversion routine which are location independent.

- :py:meth:`unix_to_skyfield_time`
- :py:meth:`datetime_to_unix`
- :py:meth:`unix_to_datetime`
- :py:meth:`datetime_to_timestr`
- :py:meth:`timestr_to_datetime`
- :py:meth:`unix_to_era`
- :py:meth:`era_to_unix`
- :py:meth:`ensure_unix`
- :py:meth:`leap_seconds_between`
- :py:meth:`time_of_day`
- :py:meth:`naive_datetime_to_utc`

Local Time Utilities
====================

Routines which are location specific are grouped into the location aware class :py:class:`Observer`.

This class can be used to calculate Local Stellar Angle (LSA), and the Local
Stellar Day (LSD). LSA is an equivalent to the Local Sidereal Time based around
the Earth Rotation Angle instead of the Greenwich Sidereal Time. This is defined
as:

.. math::
    \mathrm{LSA} = \theta + \lambda

where :math:`\theta` is the Earth Rotation Angle, and :math:`\lambda` is the
longitude. Local Stellar Day counts the stellar days (i.e. the number of cycles
of LSA) that have occured since a given start epoch. This means that the
fractional part is simply the LSA rescaled., with the integer part the number of
stellar days elapsed since that epoch. This requires the specification of the
start epoch, which is determined from a given UNIX time, the code simply picks
the first time :math:`\mathrm{LSA} = 0` after this time.

Note that the quantities LSA and LSD are not really used elsewhere. However, the
concept of a Stellar Day as a length of time is well established (`IERS
constants`_), and the Stellar Angle is an older term for the Earth Rotation
Angle (`NFA Glossary`_).

Skyfield Interface
==================

- :py:class:`SkyfieldWrapper`

This module provides an interface to Skyfield which stores the required datasets
(timescale data and an ephemeris) in a fixed location. The location is
determined by the following (in order):

- As the wrapper is initialised by passing in a ``path=<path>`` option.
- By setting the environment variable ``CAPUT_SKYFIELD_PATH``
- If neither of the above is set, the data is place in ``<path to caput>/caput/data/``

Constants
=========

:const:`SIDEREAL_S`
    Number of SI seconds in a sidereal second [s/sidereal s].
:const:`STELLAR_S`
    Approximate number of SI seconds in a stellar second (i.e. 1/86400 of a stellar day) [s/stellar s].

Why not Sidereal Time?
----------------------

Sidereal time is a long established and well known quantity that is very similar
to the Earth Rotation Angle (and the derived quantities, LSA and LSD). The
reason we don't use that is that Sidereal Time is essentially the Earth's
rotation measured with respect to the vernal equinox, but this quantity moves
significantly with respect to the fixed frame due to precession of the Earth's
pole.

The Earth Rotation Angle is measured with respect to the Celestial Intermediate
Origin (CIO), which essentially moves the minimal amount as the Earth precesses
to remain on the celestial equator. For experiments like CHIME which map the sky
as a function of the Earth's rotation, this gives the minimal coordinate shifts
when combining maps made on different days.

See `USNO Circular 179`_ for more details about the CIO based coordinates.

Accuracy
--------

Conversions between time standards (as opposed to representations of UTC) in
this module are mostly handled by Skyfield_, which has a comprehensive and
modern handling of the various effects. This includes leap seconds, which
``PyEphem`` mostly ignored, just pretending that UTC and UT1 were equivalent.

Skyfield uses Julian Dates for its internal representations. For a double
precision floating point (``np.float64``), the effective precision on
calculations is around 30 us. A technical note discussing this further is
available `here <http://aa.usno.navy.mil/software/novas/USNOAA-TN2011-02.pdf>`_.
Most conversions should be accurate to around this accuracy 0.1 ms. However, the
`era_to_unix` routine (and related routines), are accurate to only around 5 ms
at the moment as they ignore the difference in time between the Sidereal day,
and a complete cycle of ERA.

.. _Skyfield: http://rhodesmill.org/skyfield/

.. _`USNO Circular 179`: http://arxiv.org/abs/astro-ph/0602086

.. _`NFA Glossary`: http://syrte.obspm.fr/iauWGnfa/NFA_Glossary.pdf

.. _`IERS constants`: http://hpiers.obspm.fr/eop-pc/models/constants.html
"""
from datetime import datetime
import warnings

import numpy as np
from scipy.optimize import brentq
from skyfield import timelib
from skyfield.starlib import Star
from skyfield.units import Angle

from . import config
from .misc import vectorize, scalarize, listize

# The approximate length of a UT1 second in SI seconds (i.e. LOD / 86400). This was
# calculated from the IERS EOP C01 IAU2000 data, by calculating the derivative of UT1 -
# TAI from 2019.5 to 2020.5. Note that the variations in this are quite substantial,
# but it's typically 1ms over the source of a day
UT1_S = 1.00000000205

# Approximate number of seconds in a sidereal second.
# The exact value used here is from https://hpiers.obspm.fr/eop-pc/models/constants.html
# but can be derived from USNO Circular 179 Equation 2.12
SIDEREAL_S = 1.0 / 1.002737909350795 * UT1_S

# Approximate length of a stellar second
# This comes from the definition of ERA-UT1 (see IERS Conventions TR Chapter 1) giving
# the first ratio a UT1 and stellar second
STELLAR_S = 1.0 / 1.00273781191135448 * UT1_S

def _fixup_interval_and_step(t0, t1, step):
    # Work routine used by Observer._sr_work and transit_times to sanitise the
    # caller-supplied interval and initial step duration.
    #
    # Returns the interval limits converted to TT JD and the computed
    # initial step size

    # Get the ends of the search interval
    t0 = ensure_unix(t0)
    if t1 is None:
        t1 = t0 + 24 * 3600.0 * STELLAR_S
    else:
        t1 = ensure_unix(t1)
        if t1 <= t0:
            raise ValueError("End of the search interval (t1) is before the start (t0)")

    # Calculate the initial search step
    if step is None:
        if t1 - t0 >= 24 * 3600.0:
            step = 0.2
        else:
            step = (t1 - t0) / (5 * 24 * 3600.0)
    elif step * 24 * 3600 >= t1 - t0:
        raise ValueError("Initial search step is larger than the search interval")

    # Convert the UNIX start and end times into Julian Days
    t0jd, t1jd = unix_to_skyfield_time([t0, t1]).tt

    return t0jd, t1jd, step

class Observer:
    """Time calculations for a local observer.

    Parameters
    ----------
    longitude : float
        Longitude of observer in degrees.
    latitude : float
        Latitude of observer in degrees.
    altitude : float, optional
        Altitude of observer in metres.
    lsd_start : float or datetime, optional
        The zeroth LSD. If not set use the J2000 epoch start.
    sf_wrapper
        (optional) Skyfield wrapper

    Attributes
    ----------
    longitude : float
        Longitude of observer in degrees.
    latitude : float
        Latitude of observer in degrees.
    altitude : float
        Altitude of observer in metres.
    lsd_start_day : float
        UNIX time on the zeroth LSD. The actual zero point is the first time of
        `LSA = 0.0` after the `lsd_start_day`.
    """

    longitude = config.float_in_range(-180.0, 180.0, default=0.0)
    latitude = config.float_in_range(-90.0, 90.0, default=0.0)

    altitude = config.Property(proptype=float, default=0.0)

    lsd_start_day = config.utc_time(default=datetime(2000, 1, 1, 11, 58, 56))

    def __init__(self, lon=0.0, lat=0.0, alt=0.0, lsd_start=None, sf_wrapper=None):
        self.longitude = lon
        self.latitude = lat
        self.altitude = alt

        self.skyfield = skyfield_wrapper if sf_wrapper is None else sf_wrapper

        if lsd_start is not None:
            self.lsd_start_day = lsd_start

    _obs = None

    def get_current_lsd(self) -> float:
        """Get the current LSD."""
        return self.lsd(datetime_to_unix(datetime.utcnow())).astype(np.float64)

    def skyfield_obs(self):
        """Create a Skyfield topos object for the current location.

        Returns
        -------
        obs : :class:`skyfield.toposlib.Topos`
        """
        from skyfield.api import Topos

        earth = self.skyfield.ephemeris["earth"]

        if self._obs is None:
            self._obs = earth + Topos(
                latitude_degrees=self.latitude,
                longitude_degrees=self.longitude,
                elevation_m=self.altitude,
            )

        return self._obs

    @listize()
    def unix_to_lsa(self, time):
        """Calculate the Local Stellar Angle.

        This is the angle between the current meridian and the CIO, i.e. the ERA +
        longitude.

        Parameters
        ----------
        time : float
            Unix time.

        Returns
        -------
        lsa : float
        """
        era = unix_to_era(time)

        lsa = (era + self.longitude) % 360.0

        return lsa

    lsa = unix_to_lsa

    @listize()
    def lsa_to_unix(self, lsa, time0):
        """Convert a Local Stellar Angle (LSA) on a given day to a UNIX time.

        Parameters
        ----------
        lsa : scalar or np.ndarray
            Local Earth Rotation Angle degrees to convert.
        time0 : scalar or np.ndarray
            An earlier time within 24 sidereal hours. For example,
            the start of the solar day of the observation.

        Returns
        -------
        time : scalar or np.ndarray
            Corresponding UNIX time.
        """
        era = (lsa - self.longitude) % 360.0

        return era_to_unix(era, time0)

    def lsd_zero(self):
        """Return the zero point of LSD as a UNIX time.

        Returns
        -------
        lsd_zero : float
        """
        return self.lsa_to_unix(0.0, self.lsd_start_day)

    @listize()
    def unix_to_lsd(self, time):
        """Calculate the Local Stellar Day (LSD) corresponding to the given time.

        The Local Earth Rotation Day is the number of cycles of Earth Rotation
        Angle that have passed since the specified zero epoch (including
        fractional cycles).

        Parameters
        ----------
        time :  float or array of
            UNIX time

        Returns
        -------
        lsd : float or array of
        """
        # Get fractional part from LRA
        frac_part = self.unix_to_lsa(time) / 360.0

        # Calculate the approximate CSD by crudely dividing the time difference by
        # the length of a sidereal day
        approx_lsd = (time - self.lsd_zero()) / (24.0 * 3600.0 * SIDEREAL_S)

        # Subtract the accurate part, and round to the nearest integer to get the
        # number of whole days elapsed (should be accurate for a very long time)
        whole_days = np.rint(approx_lsd - frac_part)

        return whole_days + frac_part

    lsd = unix_to_lsd

    @listize()
    def lsd_to_unix(self, lsd):
        """Calculate the UNIX time corresponding to a given LSD.

        Parameters
        ----------
        lsd : float or array of
            Local Stellar Day to convert to unix

        Returns
        -------
        time :  float or array of
            UNIX time
        """
        # Find the approximate UNIX time
        approx_unix = self.lsd_zero() + lsd * 3600 * 24 * SIDEREAL_S

        # Shift to 12 hours before to give the start of the search period
        start_unix = approx_unix - 12 * 3600

        # Get the LRA from the LSD in degrees
        lsa = 360.0 * (lsd % 1.0)

        # Solve for the next transit of that RA after start_unix
        return self.lsa_to_unix(lsa, start_unix)

    @listize()
    def unix_to_lst(self, unix):
        """Calculate the apparent Local Sidereal Time for the given UNIX time.

        Parameters
        ----------
        unix : float or array of
            UNIX time in floating point seconds since the epoch.

        Returns
        -------
        lst : float or array of
            The apparent LST in degrees.
        """
        st = unix_to_skyfield_time(unix)

        return (st.gast * 15.0 + self.longitude) % 360.0

    lst = unix_to_lst

    @scalarize()
    def transit_RA(self, time):
        """Transiting RA for the observer at given Unix Time.

        Because the RA is defined with respect to the specified epoch (J2000 by
        default), the elevation actually matters here. The elevation of the
        equator is used, which minimizes this effect.

        Parameters
        ----------
        time : float or array of floats
            Time as specified by the Unix/POSIX time.

        Returns
        -------
        transit_RA : float or array of floats
            Transiting RA in degrees.

        Notes
        -----
        It is not clear that this calculation includes nutation and stellar
        aberration.  See the discussion
        `on stackoverflow <http://stackoverflow.com/questions/11970713>`_.
        Some testing does seem to indicate that these effects are accounted for.

        This calculates the RA in the given epoch which by default is J2000, but
        it might be more appropriate to use an epoch that is closer to the
        observation time. The mismatch in the celestial poles is not
        insignificant (~5 arcmin from J2000 to J2016).

        PyEphem uses all geocentric latitudes, which I don't think affects
        this calculation.
        """
        # Initialize Skyfield location object.
        obs = self.skyfield_obs()

        # Want the RA at the equator, which is much less affected by the celestial
        # pole mismatch between now and J2000 epoch.
        az = 180.0
        el = 90.0 - self.latitude
        obs.pressure = 0

        st = unix_to_skyfield_time(time)
        pos = obs.at(st).from_altaz(az_degrees=az, alt_degrees=el)
        ra, _, _ = pos.radec()  # Fetch ICRS position (effectively J2000)

        ra = np.degrees(ra.radians)

        return ra

    def transit_times(
        self, source, t0, t1=None, step=None, lower=False, return_dec=False
    ):
        """Find the transit times of the given source in an interval.

        Parameters
        ----------
        source : skyfield source or float
            The source we are calculating the transit of. This can be any body
            skyfield can observe, such as a star (`skyfield.api.Star`), planet or
            moon (`skyfield.vectorlib.VectorSum` or
            `skyfield.jpllib.ChebyshevPosition`). Additionally if a float is passed,
            this is equivalent to a body with ICRS RA given by the float, and DEC=0.
        t0 : float unix time, or datetime
            The start time to search for. Any type that can be converted to a UNIX
            time by caput.
        t1 : float unix time, or datetime, optional
            The end time of the search interval. If not set, this is 1 day after the
            start time `t0`.
        step : float or None, optional
            The initial search step in days. This is used to find the approximate
            times of transit, and should be set to something less than the spacing
            between events.  If None is passed, an initial search step of 0.2 days,
            or else one fifth of the specified interval, is used, whichever is smaller.
        lower : bool, optional
            By default this only returns the upper (regular) transit. This will cause
            lower transits to be returned instead.
        return_dec : bool, optional
            If set, also return the declination of the source at transit.

        Returns
        -------
        times : np.ndarray
            UNIX times of transits.
        dec : np.ndarray
            Only returned if `return_dec` is set. Declination of source at transit.
        """
        if isinstance(source, float):
            source = skyfield_star_from_ra_dec(source, 0.0)

        # The function to find routes for. For the upper transit we just search for
        # HA=0, for the lower transit we need to rotate the 180 -> -180 transition to
        # be at 0.
        def f(t):
            ha = self._source_ha(source, t)
            return ha if not lower else (ha % 360.0) - 180.0

        # Convert interval to Julian Days and choose the initial search step if not set
        t0jd, t1jd, step = _fixup_interval_and_step(t0, t1, step)

        # Compute transits
        transits, _ = _solve_all(f, t0jd, t1jd, step, skip_decreasing=True, xtol=1e-8)

        # Convert into UNIX times
        t_sf = self.skyfield.timescale.tt_jd(transits)
        t_unix = skyfield_time_to_unix(t_sf)

        if return_dec:
            if len(transits) > 0:
                pos = self.skyfield_obs().at(t_sf).observe(source)
                dec = pos.cirs_radec(epoch=t_sf)[1]._degrees
            else:
                dec = np.array([], dtype=np.float64)

            return t_unix, dec
        else:
            return t_unix

    def rise_set_times(self, source, t0, t1=None, step=None, diameter=0.0):
        """Find all times a sources rises or sets in an interval.

        Parameters
        ----------
        source : skyfield source
            The source we are calculating the rising and setting of.
        t0 : float unix time, or datetime
            The start time to search for. Any type that be converted to a UNIX time
            by caput.
        t1 : float unix time, or datetime, optional
            The end time of the search interval. If not set, this is 1 day after the
            start time `t0`.
        step : float or None, optional
            The initial search step in days. This is used to find the approximate
            times of risings and settings, and should be set to something less than the
            spacing between events.  If None is passed, an initial search step of
            0.2 days, or else one fifth of the specified interval, is used, whichever
            is smaller.
        diameter : float
            The size of the source in degrees. Use this to ensure the whole source is
            below the horizon. Also, if the local horizon is higher (i.e. mountains),
            this can be set to a negative value to account for this.

        Returns
        -------
        times : np.ndarray
            Source rise/set times as UNIX epoch times.
        rising : np.ndarray
            Boolean array of whether the time corresponds to a rising (True) or
            setting (False).
        """
        return self._sr_work(
            source, t0, t1, step, diameter, skip_rise=False, skip_set=False
        )

    def rise_times(self, source, t0, t1=None, step=None, diameter=0.0):
        """Find all times a sources rises in an interval.

        Parameters
        ----------
        source : skyfield source
            The source we are calculating the rising of.
        t0 : float unix time, or datetime
            The start time to search for. Any type that be converted to a UNIX time
            by caput.
        t1 : float unix time, or datetime, optional
            The end time of the search interval. If not set, this is 1 day after the
            start time `t0`.
        step : float or None, optional
            The initial search step in days. This is used to find the approximate
            times of rising, and should be set to something less than the spacing
            between events.  If None is passed, an initial search step of 0.2 days,
            or else one fifth of the specified interval, is used, whichever is smaller.
        diameter : float
            The size of the source in degrees. Use this to ensure the whole source is
            below the horizon. Also, if the local horizon is higher (i.e. mountains),
            this can be set to a negative value to account for this.

        Returns
        -------
        times : np.ndarray
            Source rise times as UNIX epoch times.
        """
        return self._sr_work(
            source, t0, t1, step, diameter, skip_rise=False, skip_set=True
        )[0]

    def set_times(self, source, t0, t1=None, step=None, diameter=0.0):
        """Find all times a sources sets in an interval.

        Parameters
        ----------
        source : skyfield source
            The source we are calculating the setting of.
        t0 : float unix time, or datetime
            The start time to search for. Any type that be converted to a UNIX time
            by caput.
        t1 : float unix time, or datetime, optional
            The end time of the search interval. If not set, this is 1 day after the
            start time `t0`.
        step : float or None, optional
            The initial search step in days. This is used to find the approximate
            times of setting, and should be set to something less than the spacing
            between events.  If None is passed, an initial search step of 0.2 days,
            or else one fifth of the specified interval, is used, whichever is smaller.
        diameter : float
            The size of the source in degrees. Use this to ensure the whole source is
            below the horizon. Also, if the local horizon is higher (i.e. mountains),
            this can be set to a negative value to account for this.

        Returns
        -------
        times : np.ndarray
            Source rise times as UNIX epoch times.
        """
        return self._sr_work(
            source, t0, t1, step, diameter, skip_rise=True, skip_set=False
        )[0]

    def _sr_work(self, source, t0, t1, step, diameter, skip_rise=False, skip_set=False):
        # A work routine factoring out common functionality

        # The function to find roots for. This is just the altitude of the source with
        # an offset for it's finite size
        def f(t):
            return self._source_alt(source, t) + diameter / 2

        # Convert interval to Julian Days and choose the initial search step if not set
        t0jd, t1jd, step = _fixup_interval_and_step(t0, t1, step)

        times, risings = _solve_all(
            f,
            t0jd,
            t1jd,
            step,
            skip_increasing=skip_rise,
            skip_decreasing=skip_set,
            xtol=1e-6,
        )

        # Convert into UNIX times
        times = skyfield_time_to_unix(self.skyfield.timescale.tt_jd(times))

        return times, risings

    def _source_ha(self, source, t):
        # Calculate the local Hour Angle of a given source
        time = self.skyfield.timescale.tt_jd(t)
        lst = time.gast * 15 + self.longitude

        ra = self.skyfield_obs().at(time).observe(source).radec(epoch=time)[0]._degrees

        return (((lst - ra) + 180) % 360) - 180

    def _source_alt(self, source, t):
        # Calculate the altitude of a given source
        from skyfield.positionlib import _to_altaz

        time = self.skyfield.timescale.tt_jd(t)
        pos = self.skyfield_obs().at(time).observe(source)

        # NOTE: we could have used `.apparent().altz()` here, but the corrections for
        # light travel time are super slow, so we're better off skipping them. To do
        # this we need to use a private Skyfield method. This isn't a great idea given
        # how unstable skyfields internal API seems to be, but it saves reinventing the
        # wheel. We'll see how it goes for now.
        alt = _to_altaz(pos, None, None)[0].degrees

        return alt

@scalarize()
def unix_to_skyfield_time(unix_time):
    """Formats the Unix time into a time that can be interpreted by Skyfield.

    Parameters
    ----------
    unix_time : float or array of.
        Unix/POSIX time.

    Returns
    -------
    time : :class:`skyfield.timelib.Time`
    """
    ts = skyfield_wrapper.timescale

    days, seconds = divmod(unix_time, 24 * 3600.0)

    # Construct Julian day and convert to calendar day
    year, month, day = timelib.calendar_date(2440588 + days)

    # Construct Skyfield time. Cheat slightly by putting all of the time of day
    # in the `second` argument.
    t = ts.utc(year, month, day, second=seconds)

    return t

@scalarize()
def skyfield_time_to_unix(skyfield_time):
    """Formats the Skyfield time into UNIX times.

    Parameters
    ----------
    skyfield_time : `skyfield.timelib.Time`
        Skyfield time.

    Returns
    -------
    time : float or array of
        UNIX time.
    """
    # TODO: I'm surprised there isn't a better way to do this. Needing to convert via a
    # datetime isn't great, but the only other ways I think can do it use private
    # routines
    return ensure_unix(skyfield_time.utc_datetime())

@scalarize()
def unix_to_era(unix_time):
    """Calculate the Earth Rotation Angle for a given time.

    The Earth Rotation Angle is the angle between the Celestial and Terrestrial
    Intermediate origins, and is a modern replacement for the Greenwich Sidereal
    Time.

    Parameters
    ----------
    unix_time : float or array of.
        Unix/POSIX time.

    Returns
    -------
    era : float or array of
        The Earth Rotation Angle in degrees.
    """
    from skyfield import earthlib

    t = unix_to_skyfield_time(unix_time)

    era = earthlib.earth_rotation_angle(t.ut1)  # in cycles

    return 360.0 * era

@scalarize()
def era_to_unix(era, time0):
    """Calculate the UNIX time for a given Earth Rotation Angle.

    The Earth Rotation Angle is the angle between the Celetial and Terrestrial
    Intermediate origins, and is a modern replacement for the Greenwich Sidereal
    Time.

    This routine is accurate at about the 1 ms level.

    Parameters
    ----------
    era : float or array of
        The Earth Rotation Angle in degrees.
    time0 : scalar or np.ndarray
        An earlier time within 24 sidereal hours. For example,
        the start of the solar day of the observation.

    Returns
    -------
    unix_time : float or array of.
        Unix/POSIX time.
    """
    era0 = unix_to_era(time0)

    diff_era_deg = (era - era0) % 360.0  # Convert from degrees in seconds (time)

    # Convert to time difference using the rough estimate of the Stellar second
    # (~50 us accuracy). Could be improved with better estimate of the Stellar
    # Second.
    diff_time = diff_era_deg * 240.0 * STELLAR_S

    # Did if any leap seconds occurred between the search start and the final value
    leap_seconds = leap_seconds_between(time0, time0 + diff_time)

    return time0 + diff_time - leap_seconds

@vectorize(otypes=[object])
def unix_to_datetime(unix_time):
    """Converts unix time to a :class:`~datetime.datetime` object.

    Equivalent to :meth:`datetime.datetime.utcfromtimestamp`.

    Parameters
    ----------
    unix_time : float
        Unix/POSIX time.

    Returns
    -------
    dt : :class:`datetime.datetime`
        datetime object from the provided time

    See Also
    --------
    :func:`datetime_to_unix`
    """
    dt = datetime.utcfromtimestamp(unix_time)

    return naive_datetime_to_utc(dt)

@vectorize(otypes=[np.float64])
def datetime_to_unix(dt):
    """Converts a :class:`~datetime.datetime` object to the unix time.

    This is the inverse of :meth:`datetime.datetime.utcfromtimestamp`.

    Parameters
    ----------
    dt : :class:`datetime.datetime`
        datetime object to convert to unix

    Returns
    -------
    unix_time : float
        Unix/POSIX time.

    See Also
    --------
    :func:`unix_to_datetime`
    :meth:`datetime.datetime.utcfromtimestamp`
    """
    # Noting that this operation is ignorant of leap seconds.
    dt = naive_datetime_to_utc(dt)
    epoch_start = naive_datetime_to_utc(datetime.utcfromtimestamp(0))
    since_epoch = dt - epoch_start
    return since_epoch.total_seconds()

@vectorize(otypes=[str])
def datetime_to_timestr(dt):
    """Converts a :class:`~datetime.datetime` to "YYYYMMDDTHHMMSSZ" format.

    Partial seconds are ignored.

    Parameters
    ----------
    dt : :class:`datetime.datetime`
        datetime object to convert to timestring

    Returns
    -------
    time_str : string
        Formated date and time.

    See Also
    --------
    :func:`timestr_to_datetime`
    """
    return dt.strftime("%Y%m%dT%H%M%SZ")

@vectorize(otypes=[object])
def timestr_to_datetime(time_str):
    """Converts date "YYYYMMDDTHHMMSS*" to a :class:`~datetime.datetime`.

    Parameters
    ----------
    time_str : string
        Formated date and time.

    Returns
    -------
    time_str : :class:`datetime.datetime`

    See Also
    --------
    :func:`datetime_to_timestr`

    """
    return datetime.strptime(time_str[:15], "%Y%m%dT%H%M%S")

@scalarize(dtype=np.int64)
def leap_seconds_between(time_a, time_b):
    """Determine how many leap seconds occurred between two Unix times.

    Parameters
    ----------
    time_a : float
        First Unix/POSIX time.
    time_b : float
        Second Unix/POSIX time.

    Returns
    -------
    int : bool
        The number of leap seconds between *time_a* and *time_b*.
    """
    # Construct the elapse UNIX time
    delta_unix = time_b - time_a

    # Construct the elapsed terrestrial time
    tt_a = unix_to_skyfield_time(time_a).tt
    tt_b = unix_to_skyfield_time(time_b).tt
    delta_tt = (tt_b - tt_a) * 24.0 * 3600

    # Calculate the shift in timescales which should only happen when leap
    # seconds are added/removed
    time_shift = delta_tt - delta_unix
    time_shift_int = np.around(time_shift).astype(int)

    # Check that the shift is an integer number of seconds. I don't know why
    # this wouldn't be true, but if it's not it means things have gone crazy
    if np.any(np.abs(time_shift - time_shift_int) > 0.01):
        raise RuntimeError(
            "Time shifts between TT and UTC does not seem to"
            + " be an integer number of seconds."
        )

    # If the differences are close then there is no leap second
    return time_shift_int

@scalarize()
def ensure_unix(time):
    """Convert the input time to Unix time format.

    Parameters
    ----------
    time : float, string, :class:`~datetime.datetime` or :class:`skyfield.timelib.Time`
        Input time, or array of times.

    Returns
    -------
    unix_time : float, or array of
        Output time.
    """
    if isinstance(time[0], datetime):
        return datetime_to_unix(time)
    elif isinstance(time[0], str):
        return datetime_to_unix(timestr_to_datetime(time))
    elif isinstance(time[0], timelib.Time):
        return datetime_to_unix(time.utc_datetime())
    elif isinstance(time, np.ndarray) and np.issubdtype(time.dtype, np.number):
        return time.astype(np.float64)
    else:
        raise TypeError("Could not convert %s into a UNIX time" % repr(type(time)))

_warned_utc_datetime = False

@vectorize(otypes=[object])
def naive_datetime_to_utc(dt):
    """Add UTC timezone info to a naive datetime.

    This only does anything if Skyfield is installed.

    Parameters
    ----------
    dt : datetime
        datetime object without 'tzinfo'

    Returns
    -------
    dt : datetime
        New datetime with `tzinfo` added.
    """
    try:
        from skyfield.api import utc

        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=utc)
    except ImportError:
        global _warned_utc_datetime

        if not _warned_utc_datetime:
            warnings.warn(
                "Skyfield not installed. Cannot add UTC timezone to datetime."
            )
            _warned_utc_datetime = True

    return dt

@vectorize()
def time_of_day(time):
    """Return the time since the start of the UTC day in seconds.

    Parameters
    ----------
    time : float (UNIX time), or datetime
        Find the start time of the day that `time` is in.

    Returns
    -------
    time : float
        Time since start of UTC day in seconds.
    """
    dt = datetime.utcfromtimestamp(ensure_unix(time))
    d = dt.replace(hour=0, minute=0, second=0, microsecond=0)
    return (dt - d).total_seconds()

class SkyfieldWrapper:
    """A wrapper to help with loading Skyfield and its data.

    Parameters
    ----------
    path : string, optional
        Directory Skyfield should save data in. If not set data will be looked
        for in `$CAPUT_SKYFIELD_PATH` or in `<path to caput>/caput/data`.
    expire : bool, optional
        Deprecated option. Skyfield no longer has a concept of expiring data. To get
        updated data you must force an explicit reload of it which can be done via
        `SkyFieldWrapper.reload`.
    ephemeris : string, optional
        The JPL ephemeris to use. Defaults to `'de421.bsp'`.
    """

    mirror_url = "https://bao.chimenet.ca/skyfield/"

    def __init__(self, path=None, expire=None, ephemeris="de421.bsp"):
        import os

        self._ephemeris_name = ephemeris

        if expire is not None:
            warnings.warn(
                "`expiry` argument deprecated as Skyfield has dropped the idea of "
                "expiring data.",
                DeprecationWarning,
            )

        if path is None:
            if "CAPUT_SKYFIELD_PATH" in os.environ:
                path = os.environ["CAPUT_SKYFIELD_PATH"]
            else:
                path = os.path.join(os.path.dirname(__file__), "data", "")

        # Defer failure if Skyfield is not available until we try to load
        # anything
        try:
            from skyfield import api

            self._load = api.Loader(path)
        except ImportError:
            pass

    _load = None

    @property
    def load(self):
        """A :class:`skyfield.iokit.Loader` object.

        This is to be used in the same way as `skyfield.api.load`,
        in case you want something other than `timescale` or `ephemeris`.
        """
        if self._load is None:
            raise RuntimeError("Skyfield is not installed.")
        return self._load

    @property
    def path(self):
        """The path to the Skyfield data."""
        return self.load.directory

    _timescale = None

    @property
    def timescale(self):
        """A :class:`skyfield.timelib.Timescale` object.

        Loaded at first call and then cached.
        """
        if self._timescale:
            return self._timescale

        try:
            self._timescale = self.load.timescale(builtin=False)
        except IOError:
            warnings.warn(
                "Could not get timescale data from an official source. Trying the "
                "CHIME mirror, but the products are likely out of date."
            )
            timescale_files = ["Leap_Second.dat", "finals2000A.all"]
            try:
                for file in timescale_files:
                    self.load.download(self.mirror_url + file)

                self._timescale = self.load.timescale(builtin=False)
            except IOError as e:
                raise IOError(
                    "Could not get ephemeris either from an existing installation at "
                    f"{self.load.directory}, an official download source or the CHIME "
                    f"mirror. If you can find a working mirror of "
                    f"{timescale_files} try using "
                    '`caput.time.skyfield_wrapper.load.download("<mirror_url>") '
                    "to download directly."
                ) from e

        return self._timescale

    _ephemeris = None

    @property
    def ephemeris(self):
        """A Skyfield ephemeris object (:class:`skyfield.jpllib.SpiceKernel`).

        Loaded at first call, and then cached.
        """
        if self._ephemeris:
            return self._ephemeris

        try:
            self._ephemeris = self.load(self._ephemeris_name)
            return self._ephemeris
        except IOError:
            warnings.warn(
                "Could not get ephemeris data from an official source. Trying the "
                "CHIME mirror, but the products are likely out of date."
            )
            try:
                self.load.download(self.mirror_url + self._ephemeris_name)
                self._ephemeris = self.load(self._ephemeris_name)
            except IOError as e:
                raise IOError(
                    "Could not get ephemeris either from an existing installation at "
                    f"{self.load.directory}, an official download source or the CHIME "
                    f"mirror. If you can find a working mirror of "
                    "{self._ephemeris_name} try using "
                    '`caput.time.skyfield_wrapper.load.download("<mirror_url>") '
                    "to download directly."
                ) from e

    def reload(self):
        """Reload the Skyfield data regardless of the `expire` setting.

        This will only load the data from the official sources.
        """
        # Download the timescale file and ephemeris
        self.load.download("finals2000A.all")
        self.load.download(self._ephemeris_name)

# Set up a module local Skyfield wrapper for time conversion functions in this
# module to use.
skyfield_wrapper = SkyfieldWrapper()

def _solve_all(f, x0, x1, dx, skip_increasing=False, skip_decreasing=False, **kwargs):
    """Find all roots of function f, within the interval [x0, x1].

    To find all the roots we need an estimate of the spacing of the roots. This is
    specified by the parameter `dx`. If this is too large, roots may be missed.

    Parameters
    ----------
    f : callable
        A numpy vectorized function which returns a single value f(x).
    x0, x1 : float
        The start and end of the interval to search.
    dx : float
        An interval known to be less than the spacing between the closest roots.
    skip_increasing, skip_decreasing : bool, optional
        Skip roots where the gradient is positive (or negative).
    **kwargs
        Passed to `scipy.optimize.brentq`. `xtol`, `rtol` and `maxiter` are probably
        the most useful.

    Returns
    -------
    roots : np.ndarray[np.float64]
        The values of the roots found.
    increasing : np.ndarray[bool]
        If the gradient at the root is positive.
    """
    # Form a grid of points to find intervals to search over
    x_init = np.linspace(x0, x1, int(np.ceil((x1 - x0) / dx)), endpoint=True)
    f_init = f(x_init)

    roots = []
    increasing = []

    # Search through intervals
    for xa, xb, fa, fb in zip(x_init[:-1], x_init[1:], f_init[:-1], f_init[1:]):
        # Entries are the same sign, so there is no solution in between.
        # NOTE: we need to deal with the case where one edge might be an exact root,
        # hence the strictly greater than 0.0
        if fa * fb > 0.0:
            continue

        is_increasing = fa < fb

        # Skip positive gradient roots
        if skip_increasing and is_increasing:
            continue

        # Skip negative gradient roots
        if skip_decreasing and not is_increasing:
            continue

        root = brentq(f, xa, xb, **kwargs)

        roots.append(root)
        increasing.append(is_increasing)

    return (np.array(roots, dtype=np.float64), np.array(increasing, dtype=bool))

def skyfield_star_from_ra_dec(ra, dec, name=""):
    """Create a Skyfield star object from an ICRS position.

    Parameters
    ----------
    ra, dec : float
        The ICRS position in degrees.
    name : str, optional
        The name of the body.

    Returns
    -------
    body : skyfield.starlib.Star
        An object representing the body.
    """
    body = Star(
        ra=Angle(degrees=ra, preference="hours"), dec=Angle(degrees=dec), names=name
    )
    return body


# dict

person = {"name":"Ali", "languages":["Python","C#"]}

result = person["name"]
result = person["languages"]
result = person["languages"][0]

print(result)

import json

person_string = '{"name":"Ali", "languages":["Python","C#"]}'

# JSON string to Dict

# result1= json.loads(person_string)

# with open("person.json") as f:
#     data = json.load(f)
#     print(data)
#     print(data['name'])

# print(type(result1))
# print(result1)

person_dict = {
    "name" : "Ali",
    "languages": ["Python", "C#"]
}

# result2 = json.dumps(person_dict)
# print(result2)

# with open("person.json","w") as f:
#     json.dump(person_dict,f)

person_dict = json.loads(person_string)

result3 = json.dumps(person_dict, indent = 4, sort_keys= True)

print(person_dict)
print(result3)


# Modular Exponentiation
def mod_exp(b: int, a: int, n: int) -> int:
    """Fast Modular Exponentiation:
    Raises 'b' to the 'a' power modulo 'n'.
    This implementation has a computational complexity of O(log2(a))."""
    res = 1
    for i in range(a.bit_length()):
        if bool((1<<i) & a): res = (res * b) % n
        b = (b**2) % n
    return res

# Multiplicative Inverse
def mul_inv(m: int, n: int) -> int:
	pass

def mul_group(n: int) -> set:
	"""" Multiplicative Group of Integers modulo 'n'.
	Returns the underlying set of the aforementioned group, which is simply the set integers less than 'n' that are coprime to 'n'."""
	return set(from_sieve(coprime_sieve(n, n)))

def mul_ord(a: int, n, cn = None) -> int:
	""" Order of 'a' in the multiplicative groups of integers modulo 'n'.
	Notes on parameters:
	1.)  'a' should be coprime to 'n'.
	2.)  If 'n' is too large to factor, then either 'n' or 'cn' should be a dictionary of prime factors and powers.
	3.)  'cn' is the Carmichael number of 'n'.  If left at 'None', it is computed by this function, but only if 'n' can be factored or already is."""
	if cn is None: cn = carmichael_num(n)
	if type(n) is not int:
		tmp = n
		n = 1
		for p, r in tmp.items(): n*= p**r
	# Store the prime factors of the Carmichael number, 'cn', in a set denoted as 'PF'.
	P = _factor(cn)

	if type(cn) is not int:
		cn = 1
		for p, r in P: cn *= p**r

	# Navigate the lattice of divisors of 'cn' ordered by divisibility, which is isomorphic to the lattice of subgroups of Zn/Z^X ordered by inclusion.

	# 'top' and 'bottom' refer to the current sublattice.
	# 'divisors' is a set of prime factors/divisors that are assigned to the dimensions of the current sublattice.
	# 'div_cur' is the set of divisors of the link(s) connecting the top of the current or parent sublattice to the top of the next or child sublattice.
	# When 'div_cur' is empty that indicates that the search is over and the current top is returned as the order.
	top, bottom = cn, 1
	divisors = set(P.keys())

	while top != bottom:
		# Check whether any element the top directly covers satisfies the congruence relation:  a**c % n = 1;  Where 'c' is the directly covered element.
		# If that relation is satisfied, add the divisor of the link connecting the top to 'c' to the set of current divisors, 'div_cur'.
		div_cur = set()
		for d in divisors:
			if mod_exp(a, top // d, n) == 1:
				div_cur.add(d)

		# Set the dimensions of the next sublattice.
		divisors = div_cur.copy()

		# Calculate the top and bottom of the next/child sublattice.
		top = top // from_set(div_cur)
		P = _factor(top); P[1] = 1
		bottom = from_dict({x: P[x] for x in set(P.keys()) - div_cur})

	return top

def from_set (X: set) -> int:
	""" Converts a set of factors into an integer.  This function merely returns the product of the numbers in 'x'."""
	r = 1
	for x in X: r *= x
	return r

def gcd (a, b , *args):
	""" Computes the greatest common divisor(GCD) of the arguments given."""
	if issubclass(type(a), dict):
		return gcd_pre_factored(a, b, *args)
	elif issubclass(type(a), int):
		return _gcd(a, b, *args)
	else: raise TypeError('Invalid Argument Type Error:\t\'gcd(...)\' only takes Integer and Mapping type arguments.')

def gcd_pre_factored (a: dict, b: dict, *args) -> dict:
	""" Computes the greatest common divisor(GCD) of the arguments given.
	This acts on dictionaries containing the prime factors and powers of an integer, a variation of the unique form that every integer has according to the
	fundemental theorem of arithmetic."""
	if len(args) > 0:
		arg_list = [a, b]
		arg_list.extend(args)
		t = a
		for n in arg_list[1:]: t = _gcd_pre_factored(t, n)
	else: t = _gcd_pre_factored(a, b)
	return t

def _gcd_pre_factored (a: dict, b: dict) -> dict:
	# The GCD is given by the intersection of the two dictionaries of prime factors and powers, which is given ...
	# ... by performing elementwise logical conjuction on each prime factor to yield the new set of keys, coupled with performing elementwise fuzzy logical conjuction, given by 'min(...)', ...
	# ... on each corresponding prime power, to yield the values associated with each key.
	return {p: min(a[p], b[p]) for p in set(a.keys()) & set(b.keys())}

def from_dict(x: dict) -> int:
	""" Converts a dictionary containing the prime factors and powers of an integer into it's integer form."""
	res = 1
	for p, r in x.items(): res *= p**r
	return res

def _gcd(a: int, b: int, *args) -> int:
	if len(args) > 0:
		arg_list = [a, b]
		arg_list.extend(args)
		t = a
		for n in arg_list[1:]: t = euclid_algo(t, n)
	else: t = euclid_algo(a, b)
	return t

def euclid_algo(a: int, b: int) -> int:
	"""Euclidean Algorithm:

	Computes the greatest common divisor(GCD) of 'a' and 'b' by
	implementing the Euclidean algorithm.
	This function has a computational complexity of log10(max(a, b))*5."""
	while b != 0:
		t = b
		b = a % b
		a = t
	return a

# Extended Euclidean Algorithm
def euclid_algo_ext(a: int, b: int) -> tuple:
	"""Extended Euclidean Algorithm:

	Computes the greatest common divisor of 'a' and 'b' as well as the
	Bezout's coefficients, 'x' and 'y', of the equation:
	gcd(a, b) = ax + by

	Returns the previously mentioned values as a 3-tuple in the form of
	(gcd(a, b), x, y) .

	NOTE:  If 'a' and 'b' are coprime and 'a' is less than 'b' then 'x'
	is the multiplicative inverse of 'a' modulo 'b'.  And 'y' is the
	multiplicative of 'b' modulo 'a' if 'b' is less than 'a'."""
	sa, sb = 1, 0
	ta, tb = 0, 1
	while b != 0:
		t = b
		q, b = divmod(a, b)
		a = t
		st = sb
		sb = sa - q*sb
		sa = st
		tt = tb
		tb = ta - q*tb
		ta = tt
	return (a, sa, ta)

def lcm(a: int, b: int, *args) -> int:
	"""Least Common Multiple:
	Computes the LCM of 'a' and 'b' and any other args passed to it.
	This function has a computational complexity of log10(max(a, b, *args))*5.
	It simply divides the product of 'a' and 'b' by the gcd(a, b) and returns the result."""
	if len(args) > 0:
		arg_list = [a, b]
		arg_list.extend(args)
		t = 1
		for n in arg_list: t = _lcm(t, n)
	else: t = _lcm(a, b)
	return t

def _lcm(a: int, b: int) -> int:
	"""Least Common Multiple:
	Computes the LCM of 'a' and 'b'.
	This function has a computational complexity of log10(max(a, b))*5.
	It simply divides the product of 'a' and 'b' by the gcd(a, b) and returns the result."""
	_gcd = euclid_algo(a,b)
	return (a // _gcd) * b

def coprime_sieve(x, n: int = 100) -> list:
	""" Takes a number, 'x', and returns a sequence of boolean elements whose index equals a number coprime to 'x' if set to 'True'.
	The length of the list/sequence is capped at the argument,'n'.
	NOTE:  The argument, 'x', can be a dictionary of prime factors and powers, a set of prime factors, or an integer."""
	try:
		if type(x) is not set: x = set(_factor(x).keys())
	except TypeError as ex:
		# Check if 'x' is set-like.
		if len(x) != len(set(x)): raise TypeError('Argument Error:\tThe argument passed to \'x\' must be a mapping, integer, or set-like type.')

	l=[True for i in range(n)]
	l[0], l[1] = False, True
	for p in x:
		for i in range(p, n, p): l[i]=False
	return l

def from_sieve(l: list) -> list:
	""" Takes a sieve, a sequence of boolean elements whose index is some type of number -- e.g. a prime number -- if it is set to 'True', and returns that list of numbers. """
	p=list()
	for i, val in enumerate(l):
		if val: p.append(i)
	return p

def sieve(n: int = 100):
	""" Prime Sieve:
	Creates a sieve that can be used to generate prime numbers up to and including 'n'.  If 'n' is prime, it will indicated as such by this sieve."""
	n += 1
	# Create a sequence of boolean values whose index equals the number being checked for primality.  If the element at index 'i' is true, 'i' is prime.
	l=[True for i in range(n)]
	l[0]=False; l[1]=False
	# Enumerate the elements in 'l'.  'p' is the index and equals the number being checked for primality.  'val' is the booleans value that indicates that 'p' is prime if it is set to 'True'.
	for p, val in enumerate(l):
		if val:
		# 'p' is prime.
			for i in range(p*2, n, p): l[i]=False
	return l

def factor(n: int) -> dict:
	""" Finds the prime factors and powers of 'n' and returns them as a dictionary with the keys beings the set of prime factors and their associated values being the prime powers.
	This implementation has a computational complexity of O(sqrt(n)).  Note that there are faster algorithms, which should be implemented later."""
	# Generate a list of primes, 'p', up to the square root of 'n' rounded up.
	from math import ceil, floor
	P = from_sieve(sieve(floor(n**(1/2))))
	pf = dict()
	for p in P:
		if p > floor(n**(1/2)): break
		n, r = divmod(n, p)
		while r == 0:
			if p in pf.keys(): pf[p] += 1
			else: pf[p] = 1
			n, r = divmod(n, p)
		n = n*p + r
	if n not in pf.keys() and n != 1: pf[n] = 1
	return pf

def _factor(x) -> dict:
	"""This function takes either a dictionary of prime factors and powers or an integer and returns a dictionary of prime factors as key and prime powers as values.
	NOTE:  This funtion is for implementors of this module (as opposed to consumers of this module). """
	pf = dict()
	if type(x) == dict: pf = x
	elif type(x) == int: pf = factor(x)
	else: raise TypeError(f'Implementation Error:\t  \'_factor(x)\' only takes types of \'dict\' and \'int\'.')
	return pf

def eulers_totient(x) -> int:
	P = _factor(x)
	res = 1
	for pf, pp in P.items():
		res *= pf**(pp-1) * (pf-1)
	return res

def carmichael_num(x) -> int:
	P = _factor(x)
	# Store the carmichael totient values of each prime factor/power of 'x' in a list, denoted as 'tot_vals'.
	tot_vals = [eulers_totient({p: r}) // 2 if p == 2 and r > 2 else eulers_totient({p: r}) for p, r in P.items()]
	if len(tot_vals) == 1: return tot_vals[0]
	return lcm(*tot_vals)

def binomial_coefficient(n: int, m: int) -> int:
	""" Binomial Coefficient
	Returns n!/(m!(n-m)!).  This is used in combinatronics and binomial theorem."""
	from math import factorial as f
	return f(n)/(f(m)*f(n-m))

def fib (n: int) -> int:
	""" Returns the n'th Fibonacci number.
	Note that this function uses zero style indexing, meaning that the first Fibonacci number has an index
	of zero (n=0)."""
	f_p, f_c= 0, 1
	for i in range(n):
		f_n = f_c + f_p
		f_p = f_c
		f_c = f_n
	return f_c

def fib_seq (n: int) -> int:
	""" Generates a Fibonacci sequence with a length of n+1.  """
	f_p, f_c= 0, 1
	yield f_c
	for i in range(n):
		f_n = f_c + f_p
		f_p = f_c
		f_c = f_n
		yield f_c


from datetime import date

from ledger.ingestion import Ingestor
from ledger.transaction import Transaction, TransactionType

class LLoydsCreditIngestor(Ingestor):
    """ Ingests csv statements for lloyds credit cards. """
    def _convert_decimal(self, val: str) -> int:
        """ Converts a decimal string to an int representing pennies. """
        spl = val.split(".")
        total = int(spl[0] or "0") * 100
        if len(spl) > 1:
            if len(spl[1]) == 1:
                spl[1] += "0"
            total += int(spl[1])
        return total

    def ingest(self):
        super(LLoydsCreditIngestor, self).ingest()
        transactions = []
        data = self.data
        if len(data) < 2:
            return
        data = data[1:]
        balance = 0
        for idx, item in enumerate(reversed(data)):
            print(f"{idx}: {item} -> {item.split(',')}")
            strdate, _, __, description, pay_out, ___ = item.split(",")
            day, month, year = [int(x) for x in strdate.split("/")]
            dateobj = date(year, month, day)

            amount = -self._convert_decimal(pay_out)
            balance += amount

            transactions.append(
                Transaction(
                    idx=idx,
                    account=f"LLOYDS_CREDIT",
                    date=dateobj,
                    ttype=TransactionType.CREDIT_CARD_PAYMENT,
                    description=description,
                    amount=amount,
                    balance=balance
                )
            )
        # raise Exception
        return transactions

    @property
    def suitable(self) -> bool:
        return self.data[0].startswith(
            "Date,Date entered,Reference,Description,Amount,"
        )


# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import Warning
import datetime

class SaleOrderLine(models.Model):
    _inherit = "sale.order.line"

    # @api.depends('product_id')
    # def _compute_purchase_order_line_id(self):
    #     for obj in self:
    #         filtre=[
    #             ('is_sale_order_line_id','=',obj.id),
    #         ]
    #         order_lines=self.env['purchase.order.line'].search(filtre)
    #         order_id = False
    #         for line in order_lines:
    #             if line.order_id.state!='cancel':
    #                 order_id=line.id
    #                 break
    #         obj.is_purchase_order_line_id = order_id

    # is_purchase_order_line_id = fields.Many2one('purchase.order.line', string=u'Ligne commande fournisseur', compute='_compute_purchase_order_line_id', readonly=True, store=False)

    is_purchase_line_id = fields.Many2one('purchase.order.line', string=u'Ligne commande fournisseur', index=True, copy=False)

    is_date_reception         = fields.Date(string=u'Date réception')
    is_livraison_directe      = fields.Boolean(string=u'Livraison directe', help=u"Si cette case est cochée, une commande fournisseur spécifique pour ce client sera créée",default=False)

    def get_fournisseur_par_defaut(self):
        now = datetime.date.today()
        suppliers=self.env['product.supplierinfo'].search([('product_tmpl_id', '=', self.product_id.product_tmpl_id.id)])
        partner=False
        for s in suppliers:
            if now>=s.date_start and now<= s.date_end:
                partner=s.name
                break
        return partner

    @api.onchange('product_id','is_livraison_directe')
    def onchange_product_id_for_date_reception(self):
        if self.is_livraison_directe:
            if self.order_id.delivery_date:
                self.is_date_reception = str(self.order_id.delivery_date)[:10]
        else:
            partner = self.get_fournisseur_par_defaut()
            if partner and partner.is_date_reception:
                self.is_date_reception = partner.is_date_reception

    @api.onchange('is_date_reception')
    def onchange_is_date_reception(self):
        if self.is_date_reception:
            partner = self.get_fournisseur_par_defaut()
            if partner:
                partner.write({'is_date_reception': self.is_date_reception})

    @api.multi
    def acceder_commande_fournisseur(self, vals):
        for obj in self:
            res= {
                'name': 'Ligne commande fournisseur',
                'view_mode': 'tree,form',
                'view_type': 'form',
                'res_model': 'purchase.order.line',
                'type': 'ir.actions.act_window',
                'domain': [
                    ('id','=',obj.is_purchase_line_id.id),
                ],
            }
            return res

class SaleOrder(models.Model):
    _inherit = "sale.order"

    is_commande_soldee = fields.Boolean(string=u'Commande soldée', default=False, copy=False, help=u"Cocher cette case pour indiquer qu'aucune nouvelle livraison n'est prévue sur celle-ci")

    @api.depends('order_line')
    def _compute_is_creer_commande_fournisseur_vsb(self):
        for obj in self:
            vsb = False
            for line in obj.order_line:
                if not line.is_purchase_line_id.id:
                    vsb=True
                    break
            obj.is_creer_commande_fournisseur_vsb=vsb

    is_creer_commande_fournisseur_vsb = fields.Boolean(string=u'Créer commande fournisseur', compute='_compute_is_creer_commande_fournisseur_vsb', readonly=True, store=False)

    @api.multi
    def creer_commande_fournisseur_action(self):
        for obj in self:
            if not len(obj.order_line):
                raise Warning(u"Il n'y a aucune ligne de commandes à traiter !")
            for line in obj.order_line:
                if not line.is_date_reception:
                    raise Warning(u"La date de réception n'est pas renseignée sur toutes les lignes")
            now = datetime.date.today()
            for line in obj.order_line:
                suppliers=self.env['product.supplierinfo'].search([('product_tmpl_id', '=', line.product_id.product_tmpl_id.id)])
                partner_id=False
                supplierinfo=False
                for s in suppliers:
                    if now>=s.date_start and now<= s.date_end:
                        supplierinfo=s
                        break
                if supplierinfo:
                    partner_id = supplierinfo.name.id
                    date_reception = str(line.is_date_reception)
                    date_planned  = date_reception+' 08:00:00'
                    filtre=[
                        ('partner_id'  ,'='   , partner_id),
                        ('state'       ,'='   , 'draft'),
                        ('date_planned','>=', date_reception+' 00:00:00'),
                        ('date_planned','<=', date_reception+' 23:59:59'),
                    ]
                    if line.is_livraison_directe:
                        filtre.append(('delivery_adress','=', obj.partner_shipping_id.id))
                    orders=self.env['purchase.order'].search(filtre,limit=1)
                    if orders:
                        order=orders[0]
                    else:
                        vals={
                            'partner_id'  : partner_id,
                        }
                        order=self.env['purchase.order'].create(vals)
                        if order:
                            order.onchange_partner_id()
                            if line.is_livraison_directe:
                                order.delivery_adress = obj.partner_shipping_id.id
                    print(order,order.name)

                    #** Création des lignes ************************************
                    filtre=[
                        ('order_id'  ,'=', order.id),
                        ('product_id','=', line.product_id.id),
                    ]
                    order_lines=self.env['purchase.order.line'].search(filtre,limit=1)
                    if not order_lines:
                        if order:
                            vals={
                                'order_id'    : order.id,
                                'product_id'  : line.product_id.id,
                                'name'        : line.name,
                                'product_qty' : line.product_uom_qty,
                                'product_uom' : line.product_uom.id,
                                'date_planned': date_planned,
                                'price_unit'  : line.price_unit,
                                #'is_sale_order_line_id': line.id,
                            }
                            order_line=self.env['purchase.order.line'].create(vals)
                            order_line.onchange_product_id()
                    else:
                        order_line = order_lines[0]

                    if order_line:
                        line.is_purchase_line_id=order_line.id
                        filtre=[
                            ('is_purchase_line_id'  ,'=', order_line.id),
                        ]
                        order_lines=self.env['sale.order.line'].search(filtre)
                        qty=0
                        for l in order_lines:
                            qty+=l.product_uom_qty
                        order_line.product_qty = qty
                    # ***********************************************************

    # @api.multi
    # def creer_commande_fournisseur_action(self):
    #     for obj in self:
    #         if not len(obj.order_line):
    #             raise Warning(u"Il n'y a aucune ligne de commandes à traiter !")
    #         for line in obj.order_line:
    #             if not line.is_date_reception:
    #                 raise Warning(u"La date de réception n'est pas renseignée sur toutes les lignes")
    #         now = datetime.date.today()
    #         for line in obj.order_line:
    #             suppliers=self.env['product.supplierinfo'].search([('product_tmpl_id', '=', line.product_id.product_tmpl_id.id)])
    #             partner_id=False
    #             supplierinfo=False
    #             for s in suppliers:
    #                 if now>=s.date_start and now<= s.date_end:
    #                     supplierinfo=s
    #                     break
    #             if supplierinfo:
    #                 partner_id = supplierinfo.name.id
    #                 date_reception = str(line.is_date_reception)
    #                 date_planned  = date_reception+' 08:00:00'
    #                 filtre=[
    #                     ('partner_id'  ,'='   , partner_id),
    #                     ('state'       ,'='   , 'draft'),
    #                     ('date_planned','>=', date_reception+' 00:00:00'),
    #                     ('date_planned','<=', date_reception+' 23:59:59'),
    #                 ]
    #                 if line.is_livraison_directe:
    #                     filtre.append(('delivery_adress','=', obj.partner_shipping_id.id))
    #                 orders=self.env['purchase.order'].search(filtre,limit=1)
    #                 if orders:
    #                     order=orders[0]
    #                 else:
    #                     vals={
    #                         'partner_id'  : partner_id,
    #                     }
    #                     order=self.env['purchase.order'].create(vals)
    #                     if order:
    #                         order.onchange_partner_id()
    #                         if line.is_livraison_directe:
    #                             order.delivery_adress = obj.partner_shipping_id.id

    #                 #** Création des lignes ************************************
    #                 filtre=[
    #                     ('order_id'  ,'='   , order.id),
    #                     ('is_sale_order_line_id','=',line.id),
    #                 ]
    #                 order_lines=self.env['purchase.order.line'].search(filtre)
    #                 if not order_lines:
    #                     if order:
    #                         vals={
    #                             'order_id'    : order.id,
    #                             'product_id'  : line.product_id.id,
    #                             'name'        : line.name,
    #                             'product_qty' : line.product_uom_qty,
    #                             'product_uom' : line.product_uom.id,
    #                             'date_planned': date_planned,
    #                             'price_unit'  : line.price_unit,
    #                             'is_sale_order_line_id': line.id,
    #                         }
    #                         order_line=self.env['purchase.order.line'].create(vals)
    #                         order_line.onchange_product_id()
    #                         order_line.product_qty = line.product_uom_qty
    #                 #***********************************************************


# Goutham Naveen Babu - Full stack Challenge

import requests
from bs4 import BeautifulSoup     # imports
import csv

file = open('output.csv', 'w')     # set up csv output file
writer = csv.writer(file)
writer.writerow(['Questions'])

page = requests.get('https://www.cheatsheet.com/gear-style/20-questions-to-ask-siri-for-a-hilarious-response.html/')
soup = BeautifulSoup(page.content, 'html.parser')
row = soup.find_all('h2')        # finding only the questions in web page
writer.writerow([row])           # writing to csv


# 1081. Smallest Subsequence of Distinct Characters
class Solution:
    def smallestSubsequence(self, text: str) -> str:
        last = {c: i for i, c in enumerate(text)}
        stack = []
        for i, c in enumerate(text):
            if c in stack: continue
            while stack and stack[-1] > c and i < last[stack[-1]]:
                stack.pop()
            stack.append(c)
        return "".join(stack)

leetcode/0-250/283-1081. Smallest Subsequence of Distinct Characters.py


from functools import lru_cache
from typing import List, Dict
import csv

@lru_cache
def read(path: str) -> List[Dict]:
    with open(path, mode='r') as file:
        jobs_reader = csv.DictReader(file)
        jobs_list = []
        for job in jobs_reader:
            jobs_list.append(job)
        return jobs_list

def get_unique_job_types(path: str) -> List[str]:
    jobs_reader = read(path)
    unique_job_type = set()
    for job in jobs_reader:
        unique_job_type.add(job['job_type'])
    return unique_job_type

def filter_by_job_type(jobs: List[Dict], job_type: str) -> List[Dict]:
    jobs_types = []
    for job in jobs:
        if job['job_type'] == job_type:
            jobs_types.append(job)
    return jobs_types


from turtle import Turtle

class Player(Turtle):
    def __init__(self):
        super().__init__()
        self.shape("turtle")#forma tortuga
        self.penup()
        self.shapesize(2,2)#dimensionesTortuga
        self.setheading(90)#orientacionTortuga
        self.goto(0,-200)#usar goto para movimiento, crear funciones
        self.color("turquoise")
        self.missiles = []

    def turn_right(self):
        new_heading = self.heading() - 30
        self.setheading(new_heading)

    def turn_left(self):
        new_heading = self.heading() + 30
        self.setheading(new_heading)

    def create_missile(self):
        new_missile = Turtle()
        new_missile.penup()
        new_missile.goto(0,-200)
        new_missile.color("green")
        new_missile.setheading(self.heading())
        self.missiles.append(new_missile)

    def fire(self):
        for m in self.missiles:
            m.forward(10)


'''
readData_V3.py
written by: Hannah Holt

NEEDS PYTHON 2.7 TO COMPILE AND RUN

readData.py reads in a specified text file containing UT, gas number densities, and density errors for a certain orbit of the DE-2 satellite.
File contains values for O, He, N2, N and Ar as well as other orbit info

VERSION 2.0 UPDATES
    - removed anything that had to do with combining plots. We don't need this tool
    - changed the normalization feature to use a pure helium scale height instead of MSIS.

VERSION 3.0 UPDATES
    - after talking to Alan Burns and Wenbin Wang, suggestions were made to instead of normalizing He to a specific altitude, compare 2 days before
    and two days after a major ETA event and subtract those from the main event to see
    - added in the ability to plot helium densities vs invariant latitude. This gets rid of magnetic equator errors based on
    the orbits being on different longitudes (even though they have same LT)
'''

from __future__ import print_function, division
import sys, os
from pyglow.pyglow import Point  # for MSIS total mass density
from datetime import datetime
import numpy as np
import matplotlib.pyplot as plt
import csv

def print_header():
    print('\n**********************************************')
    print(os.path.basename(__file__))
    print('Written by: Hannah Holt')
    print('Last updated: 6/21/2018')
    print('**********************************************\n')

def plotData(x_values, y_values, filenames, LT_name):
    '''
    This function plots the given x and y values for a given gas and orbit number file.

    :param x_values:    array of values to plot on x axis. This could be lat, lon, LST, etc.
    :param y_values:    array of y values for the specific gas. This will usually be density
    :param filenames:    array of input files for graph title

    :return: plots the data and returns 0
    '''

    print("Done reading files. Plotting...")

    name = []

    # GRAPH LABELING....
    for i in range(0, len(filenames)):
        temp = filenames[i]
        temp = temp[4:]
        temp = temp.replace("_orbit", " (#")
        temp = temp.replace(".txt", ")")
        temp = temp.replace("_", "Day ")
        name.append(temp)

    y_label = whatplot + ' ' + y_name + ' Difference ' + ' [1/cm^3]'
    x_label = x_name + ' [deg]'
    graphTitle = 'Density Change of ' + name[1] + ' - ' + name[0] + '\nLT ~ ' + str(LT_name)

    if average:
        plt.plot(x_values, y_values, label='Raw DE2')
        plt.plot(x_values_avg, y_values_avg, label='Avg')
    else:
        plt.plot(x_values, y_values, label='Raw DE2')

    if x_lower != 0:
        plt.xlim(x_lower, x_upper)
    if y_lower != 0:
        plt.ylim(y_lower, y_upper)

    plt.ticklabel_format(style='sci', axis='y', scilimits=(0, 0))
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.title(graphTitle)
    plt.legend()
    plt.grid()
    plt.show()

    return 0

def dens_difference(dens_1, dens_2, lat_1, lat_2, lon_1, lon_2, LST_1, LST_2, split_1, split_2):
    """
    Takes in two density arrays and finds the realtive difference between the values for the same latitude

    Arguments:
        dens_1  :   density arr of first file [cm^-3]
        dens_2  :   density arr of second file [cm^-3]
        lat_1   :   latitude arr of first file [deg]
        lat_2   :   latitude arr of second file [deg]
        LST_1   :   local time arr of first file
        LST_2   :   local time arr of second file
        split_1 :   LT split integer for first file
        split_2 :   LT split integer for second file

    Returns:
        lat_diff    :   even though the latitudes will be roughly the same for consecutive files, this returns the average
        dens_diff   :   relative difference between the density values
        LT_name     :   the average LT value for the two orbits. Used later for plotting
    """

    # check to see if we want MLAT instead
    if x_name == "Magnetic Latitude":
        print('Changing to Geomagnetic Coordinates...')
        lat_1 = get_MLAT(lat_1, lon_1)
        lat_2 = get_MLAT(lat_2, lon_2)

    # only use the LT around 18
    if LST_1[split_1] > 12:
        length_1 = len(LST_1[split_1:])
        start1 = split_1
    else:
        length_1 = len(LST_1[:split_1])
        start1 = 0
        print("*** UPDATE: Changing 1st file split...")
        print("New LT1 = ", LST_1[start1])

    if LST_2[split_2] > 12:
        length_2 = len(LST_2[split_2:])
        start2 = split_2
    else:
        length_2 = len(LST_2[:split_2])
        start2 = 0
        print("*** UPDATE: Changing 2nd file split...")
        print("New LT2 = ", LST_2[start2])

    # now compare lengths - difference density array will be size of the smaller one
    if length_1 < length_2:
        size = length_1
    else:
        size = length_2

    # print('1at1[start1], lat2[start2] = ', lat_1[start1], lat_2[start2])
    # print('size = ', size)
    # print('start1 start2 =', start1, start2)
    # print('length to the end of 1 =', len(dens_1[start1:]))
    # print('length to the end of 2 =', len(dens_2[start2:]))

    # need to find latitude bin number where they start to overlap
    if lat_1[start1] > lat_2[start2]:
        for i in range(0, size):
            if lat_1[start1 + i] <= lat_2[start2]:
                start1 += i
                if len(dens_1[start1:start1+size]) < size:
                    size = len(dens_1[start1:start1+size])                                 # size of overlap shrinks
                break
    else:
        for i in range(0, size):
            if lat_2[start2 + i] <= lat_1[start1]:
                start2 += i
                if len(dens_2[start2:start2+size]) < size:
                    size = len(dens_2[start2:start2+size])                                 # size of overlap shrinks
                break

    # print("\nNEW VALUES.....")
    # print('1at1[start1], lat2[start2] = ', lat_1[start1], lat_2[start2])
    # print('size = ', size)
    # print('start1 start2 =', start1, start2)
    # print('length to the end of 1 =', len(dens_1[start1:]))
    # print('length to the end of 2 =', len(dens_2[start2:]))
    #
    # print('lat2[start stop] = ', lat_2[start2: start2+size])
    # print('lat1[start stop] = ', lat_1[start1: start1 + size])

    dens_diff = np.zeros(size)                      # difference between densities at same latitude
    lat_overlap = np.zeros(size)
    LT_value = (LST_1[start1] + LST_2[start2])/2

    # now go through and iterate to find the differences
    for i in range(0, size):
        dens_diff[i] = dens_2[start2 + i] - dens_1[start1 + i]

        # DE satellite will have basically the same latitude for the overlapping parts
        lat_overlap[i] = 0.5 * (lat_2[start2 + i] + lat_1[start1 + i])

    if compare_dens:
        plt.plot(lat_1[start1: start1+size], dens_1[start1:start1+size], label='Orbit 1608')
        plt.plot(lat_2[start2: start2+size], dens_2[start2:start2+size], label='Orbit 1614')
        plt.xlabel('Latitude [deg]')
        plt.ylabel('He Density [1/cm^3]')
        plt.grid()
        plt.legend()
        plt.show()

    return lat_overlap, dens_diff, LT_value

def find_split(LST_arr):
    '''
    Since DE2 is a polar orbiter, it will drastically change local times (or longitude) when it passes over the poles. We want to find when this happens

    :param LST_arr: the array of LT values for a specific orbit. Find when LT_n+1 - LT_n > x.
    :return: an integer that marks the first location AFTER the split in the array from 0 to len(arr)
    '''

    dLT = 1  # the LT difference cutoff. I arbitrarily choose this
    split = 0
    for i in range(1, len(LST_arr)):
        if (abs(LST_arr[i] - LST_arr[i - 1]) > dLT):
            split = i
            break
    print("LT split = ", LST_arr[split])
    return split

def avg(array, num_points):
    '''
    This function averages the given array over the specified number of points
    :param array: a numpy array of values
    :param num_points: an integer that tells how many points you would like to average over.
    :return: an array with len(array) - num_points + 1 values
    '''
    average = np.zeros(len(array) - num_points + 1)

    for i in range(0, len(average)):
        sum = 0.0
        for j in range(0, num_points):
            sum += array[i+j]
        average[i] = sum/num_points

    return average

def check_lon(lon_1, lat_1, lon_2, lat_2):
    '''

    :param lon_1: longitude values of first orbit file
    :param lat_1: geographical latitude values of first orbit file
    :param lon_2: "       "          second orbit file
    :param lat_2: "       "          second orbit file
    :return: plots the  latitude of the magnetic equator and two orbits (where they overlap) as a function of longitude
    '''

    # read in the magnetic coordinates
    mag_filename = 'mag_eq_lon_lat.txt'
    mag_lon = []
    mag_lat = []
    with open(mag_filename) as csvfile:
        readCSV = csv.reader(csvfile, delimiter=',')
        for line in readCSV:
            mag_lon.append(float(line[0]))
            mag_lat.append(float(line[1]))
    csvfile.close()

    # plot
    fig, ax = plt.subplots()
    label = []                              # legend labels
    for i in range(0, len(filenames)):
        temp = filenames[i]
        temp = temp[4:]
        temp = temp.replace("_orbit", " (#")
        temp = temp.replace(".txt", ")")
        temp = temp.replace("_", "Day ")
        label.append(temp)

    plt.plot(mag_lon, mag_lat, label='Magnetic Eq.')
    plt.plot(lon_1, lat_1, label=label[0])
    plt.plot(lon_2, lat_2, label=label[1])
    plt.xlabel("Geographic Longitude [deg]")
    plt.ylabel("Geographic Latitude [deg]")
    plt.title('DE 2 Orbit Comparison')
    plt.xlim(-180, 180)
    plt.ylim(-90, 90)
    ax.set_xticks(np.arange(-180, 181, step=20))
    plt.grid()
    plt.legend()
    plt.show()

    return 0

def get_MLAT(DE_lat, DE_lon):
    '''

    :param DE_lat: array of DE geographical latitudes for the orbit
    :param DE_lon: corresponding DE geographical longitudes for each of the latitudes
    :return: MLAT:  an array that gives the corresponding magnetic latitude for each of the longitudes
    '''

    #coordinates for the dip equator
    mag_lon_eq = []
    mag_lat_eq = []

    MLAT = np.zeros(len(DE_lat))        # magnetic latitude values to return
    # lat_values = np.zeros(len(DE_lat))

    mag_filename = 'mag_eq_lon_lat.txt'
    with open(mag_filename) as csvfile:
        readCSV = csv.reader(csvfile, delimiter=',')
        for line in readCSV:
            mag_lon_eq.append(float(line[0]))
            mag_lat_eq.append(float(line[1]))
    csvfile.close()

    for i in range(0, len(DE_lon)):
        for j in range(0, len(mag_lon_eq)):
            if int(DE_lon[i]) == mag_lon_eq[j]:
                MLAT[i] = DE_lat[i] - mag_lat_eq[j]
                # lat_values[i] = mag_lat_eq[j]

    # plt.plot(DE_lon, DE_lat, label='DE Lat')
    # plt.plot(DE_lon, lat_values, label='Mag. Eq.')
    # plt.plot(DE_lon, MLAT, label='DE MLat')
    # plt.xlabel("Longitude [deg]")
    # plt.legend()
    # plt.grid()
    # plt.show()

    return MLAT

# ----------------------- VARIABLES TO CHANGE -------------------------------------------------------
# ----------------------------------------------------------------------------------------------------

directory = './NACSfiles/outputs/'
filenames = ['1983_030_orbit8273.txt', '1983_029_orbit8257.txt']                # subtracts first file data from second
whatplot = "HE"                 # to specify what species to plot, can be: O, N2, HE, or AR
x_name = "Latitude"             # can be: 'Latitude' or 'Magnetic Latitude'
y_name = "Density"              # can be: Density
average = True                  # if you want to average the density values
npts = 75                       # number of points to average over
compare_lon = True              # if you want to plot the longitudinal comparison
compare_dens = True             # if you want to compare the individual densities
x_lower = 40                    # upper and lower x limits for graphing, zero = autoscale
x_upper = -40
y_upper = 0
y_lower = 0
# ----------------------------------------------------------------------------------------------------

print_header()

delimiter = '\t'                # file delimiter
hour = 3600000                  # ms in an hr
numFiles = len(filenames)       # number of files to read

if numFiles != 2:
    print('ERROR: Program needs two data files to compare.')
    sys.exit()

for i in range(0, numFiles):
    if not os.path.isfile(directory + filenames[i]):
        print("ERROR. Read file", filenames[i], "does not exist.")
        sys.exit()

for j in range(0, numFiles):
    fread = open(directory + filenames[j], 'r')
    print('.....')
    print('Opening file', filenames[j])

    # get number of lines and initialize data. Time is in UT [ms], densities are 1/cm^3, errors are %
    data_lines = 0
    for line in fread:
        data_lines += 1
    fread.close()
    data_lines -= 2  # don't care about first two lines (header)

    time = np.zeros(data_lines)
    O_dens, O_err = np.zeros(data_lines), np.zeros(data_lines)
    N2_dens, N2_err = np.zeros(data_lines), np.zeros(data_lines)
    He_dens, He_err = np.zeros(data_lines), np.zeros(data_lines)
    N_dens, N_err = np.zeros(data_lines), np.zeros(data_lines)
    Ar_dens, Ar_err = np.zeros(data_lines), np.zeros(data_lines)
    alt, lat, lon, LST = np.zeros(data_lines), np.zeros(data_lines), np.zeros(data_lines), np.zeros(data_lines)

    # ----------- READ IN DATA-----------------------------------------------------------------
    # -----------------------------------------------------------------------------------------

    fread = open(directory + filenames[j], 'r')

    # skip header
    fread.next()
    fread.next()

    k = 0
    # read in all the values from the file
    for line in fread:
        for i in range(0, 16):
            if i == 0:
                time[k] = float((line.split(delimiter)[i])) / hour
            elif i == 1:
                O_dens[k] = float(line.split(delimiter)[i])
            elif i == 2:
                O_err[k] = float(line.split(delimiter)[i])
            elif i == 3:
                N2_dens[k] = float(line.split(delimiter)[i])
            elif i == 4:
                N2_err[k] = float(line.split(delimiter)[i])
            elif i == 5:
                He_dens[k] = float(line.split(delimiter)[i])
            elif i == 6:
                He_err[k] = float(line.split(delimiter)[i])
            elif i == 7:
                N_dens[k] = float(line.split(delimiter)[i])
            elif i == 8:
                N_err[k] = float(line.split(delimiter)[i])
            elif i == 9:
                Ar_dens[k] = float(line.split(delimiter)[i])
            elif i == 10:
                Ar_err[k] = float(line.split(delimiter)[i])
            elif i == 11:
                continue  # don't care about orbit number
            elif i == 12:
                alt[k] = float(line.split(delimiter)[i])
            elif i == 13:
                lat[k] = float(line.split(delimiter)[i])
            elif i == 14:
                lon[k] = float(line.split(delimiter)[i])
            elif i == 15:
                LST[k] = float(line.split(delimiter)[i])
            # elif i == 18:
            #     inv_lat[k] = float(line.split(delimiter)[i])
            else:
                continue
        k += 1

    fread.close()

    # --------------------------------------------------------------------------------

    # we are on the first file
    if j == 0:
        alt_1 = alt
        lat_1 = lat
        lon_1 = lon
        LST_1 = LST
        split_1 = find_split(LST)
        print("First file split occurred at:", split_1, '\n')

        if y_name == 'Density':
            if whatplot == 'HE':
                dens_1 = He_dens
            elif whatplot == 'O':
                dens_1 = O_dens
            elif whatplot == 'N2':
                dens_1 = N2_dens
            elif whatplot == 'N':
                dens_1 = N_dens
            elif whatplot == 'Ar':
                dens_1 = Ar_dens
            else:
                print('ERROR: Invalid gas constituent.')
                sys.exit()
        else:
            print('ERROR: Invalid y axis.')
            sys.exit()

    # we are on the second file
    else:
        alt_2 = alt
        lat_2 = lat
        lon_2 = lon
        LST_2 = LST
        split_2 = find_split(LST)
        print("Second file split occurred at:", split_2, '\n')

        if y_name == 'Density':
            if whatplot == 'HE':
                dens_2 = He_dens
            elif whatplot == 'O':
                dens_2 = O_dens
            elif whatplot == 'N2':
                dens_2 = N2_dens
            elif whatplot == 'N':
                dens_2 = N_dens
            elif whatplot == 'Ar':
                dens_2 = Ar_dens
            else:
                print('ERROR: Invalid gas constituent.')
                sys.exit()
        else:
            print('ERROR: Invalid y axis.')
            sys.exit()

# end j in range (0, numfiles)

# ----------- POST PROCESSING + PLOTTING ---------------------------------------------------
#  -----------------------------------------------------------------------------------------

# now find the difference between the two density values
x_values, y_values, LT_name = dens_difference(dens_1, dens_2, lat_1, lat_2, lon_1, lon_2, LST_1, LST_2, split_1, split_2)

if compare_lon:
    check_lon(lon_1, lat_1, lon_2, lat_2)

if average:
    x_values_avg = avg(x_values, npts)
    y_values_avg = avg(y_values, npts)

plotData(x_values, y_values, filenames, LT_name)


import asyncio
import logging

from sqlalchemy import select

from accounting import database
from accounting.models import Account, Payment, PaymentStatus, Transaction, BillingCycle

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

async def main():
    await database.setup(database.Settings())
    async with database.create_session() as session:
        pending_payment_ids = (await session.execute(
            select(Payment.id).where(Payment.status == PaymentStatus.pending)
        )).scalars().all()
    for payment_id in pending_payment_ids:
        async with database.create_session() as session:
            payment, transaction, billing_cycle, account = (await session.execute(
                select(
                    Payment,
                    Transaction,
                    BillingCycle,
                    Account,
                ).join(
                    Transaction, onclause=Transaction.id == Payment.transaction_id,
                ).join(
                    BillingCycle, onclause=BillingCycle.id == Transaction.billing_cycle_id,
                ).join(
                    Account, onclause=Account.id == BillingCycle.account_id,
                ).where(
                    Payment.id == payment_id,
                    Payment.status == PaymentStatus.pending,
                ).with_for_update(
                    of=Payment,
                )
            )).one()
        logger.info(
            'Pay %s$ to %s for %s - %s',
            (transaction.credit - transaction.debit) / 100,
            account.full_name,
            billing_cycle.opened_at,
            billing_cycle.closed_at,
        )
        payment.status = PaymentStatus.completed
        session.add(payment)
        await session.commit()

if __name__ == '__main__':
    asyncio.run(main(), debug=True)


import math
from typing import Callable, Tuple, Optional
from functools import partial
from torch import nn, Tensor
from torch.nn import functional as F
from torchvision.ops import StochasticDepth

class Lambda(nn.Module):
    """An utility Module, it allows custom function to be passed

    Args:
        lambd (Callable[Tensor]): A function that does something on a tensor

    Examples:
        >>> add_two = Lambda(lambd x: x + 2)
        >>> add_two(Tensor([0])) // 2
    """

    def __init__(self, lambd: Callable[[Tensor], Tensor]):

        super().__init__()
        self.lambd = lambd

    def forward(self, x: Tensor) -> Tensor:
        return self.lambd(x)

class Conv2dPad(nn.Conv2d):
    """2D Convolutions with different padding modes.

    'auto' will use the kernel_size to calculate the padding
    'same' same padding as TensorFLow. It will dynamically pad the image based on its size

    Args:
        mode (str, optional): [description]. Defaults to 'auto'.
    """

    def __init__(self, *args, mode: str = "auto", padding: int = 0, **kwargs):

        super().__init__(*args, **kwargs)
        self.mode = mode
        # dynamic add padding based on the kernel_size
        if self.mode == "auto":
            self.padding = (
                self._get_padding(padding)
                if padding != 0
                else (self.kernel_size[0] // 2, self.kernel_size[1] // 2)
            )

    def _get_padding(self, padding: int) -> Tuple[int, int]:
        return (padding, padding)

    def forward(self, x: Tensor) -> Tensor:
        if self.mode == "same":
            ih, iw = x.size()[-2:]
            kh, kw = self.weight.size()[-2:]
            sh, sw = self.stride
            # change the output size according to stride
            oh, ow = math.ceil(ih / sh), math.ceil(iw / sw)
            pad_h = max(
                (oh - 1) * self.stride[0] + (kh - 1) * self.dilation[0] + 1 - ih, 0
            )
            pad_w = max(
                (ow - 1) * self.stride[1] + (kw - 1) * self.dilation[1] + 1 - iw, 0
            )
            if pad_h > 0 or pad_w > 0:
                x = F.pad(
                    x, [pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2]
                )
            return F.conv2d(
                x,
                self.weight,
                self.bias,
                self.stride,
                self.padding,
                self.dilation,
                self.groups,
            )

        else:
            return super().forward(x)

class ConvNormAct(nn.Sequential):
    """Utility module that stacks one convolution layer, a normalization layer and an activation function.

    Example:
        >>> ConvNormAct(32, 64, kernel_size=3)
            ConvNormAct(
                (conv): Conv2dPad(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                (norm): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                (act): ReLU()
            )

        >>> ConvNormAct(32, 64, kernel_size=3, normalization = None )
            ConvNormAct(
                (conv): Conv2dPad(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                (act): ReLU()
            )

        >>> ConvNormAct(32, 64, kernel_size=3, activation = None )
            ConvNormAct(
                (conv): Conv2dPad(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                (norm): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
            )

    We also provide additional modules built on top of this one: `ConvBn`, `ConvAct`, `Conv3x3BnAct`
    Args:
            out_features (int): Number of input features
            out_features (int): Number of output features
            conv (nn.Module, optional): Convolution layer. Defaults to Conv2dPad.
            normalization (nn.Module, optional): Normalization layer. Defaults to nn.BatchNorm2d.
            activation (nn.Module, optional): Activation function. Defaults to nn.ReLU.
    """

    def __init__(
        self,
        in_features: int,
        out_features: int,
        conv: nn.Module = Conv2dPad,
        activation: Optional[nn.Module] = nn.ReLU,
        normalization: Optional[nn.Module] = nn.BatchNorm2d,
        bias: bool = False,
        **kwargs
    ):
        super().__init__()
        self.add_module("conv", conv(in_features, out_features, **kwargs, bias=bias))
        if normalization:
            self.add_module("norm", normalization(out_features))
        if activation:
            self.add_module("act", activation())

class ConvNormRegAct(nn.Sequential):
    """Utility module that stacks one convolution layer, a normalization layer, a regularization layer and an activation function.

    Example:
        >>> ConvNormDropAct(32, 64, kernel_size=3)
            ConvNormDropAct(
                (conv): Conv2dPad(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
                (norm): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
                (reg): StochasticDepth(p=0.2)
                (act): ReLU()
            )
    """

    def __init__(
        self,
        in_features: int,
        out_features: int,
        conv: nn.Module = Conv2dPad,
        activation: Optional[nn.Module] = nn.ReLU,
        normalization: Optional[nn.Module] = nn.BatchNorm2d,
        regularization: Optional[nn.Module] = partial(StochasticDepth, mode="batch"),
        p: float = 0.2,
        bias: bool = False,
        **kwargs
    ):
        super().__init__()
        self.add_module("conv", conv(in_features, out_features, **kwargs, bias=bias))
        if normalization:
            self.add_module("norm", normalization(out_features))
        if regularization:
            self.add_module("reg", regularization(p=p))
        if activation:
            self.add_module("act", activation())

ReLUInPlace = partial(nn.ReLU, inplace=True)

class NormActConv(nn.Sequential):
    """A Sequential layer composed by a normalization, an activation and a convolution layer. This is usually known as a 'Preactivation Block'

    Args:
        in_features (int): Number of input features
        out_features (int): Number of output features
        conv (nn.Module, optional): [description]. Defaults to Conv2dPad.
        normalization (nn.Module, optional): [description]. Defaults to nn.BatchNorm2d.
        activation (nn.Module, optional): [description]. Defaults to nn.ReLU.
    """

    def __init__(
        self,
        in_features: int,
        out_features: int,
        conv: nn.Module = Conv2dPad,
        normalization: Optional[nn.Module] = nn.BatchNorm2d,
        activation: Optional[nn.Module] = ReLUInPlace,
        *args,
        **kwargs
    ):
        super().__init__()
        if normalization:
            self.add_module("norm", normalization(in_features))
        if activation:
            self.add_module("act", activation())
        self.add_module("conv", conv(in_features, out_features, *args, **kwargs))

ConvBnAct = partial(ConvNormAct, normalization=nn.BatchNorm2d)
ConvBn = partial(ConvBnAct, activation=None)
ConvAct = partial(ConvBnAct, normalization=None, bias=True)
Conv3x3BnAct = partial(ConvBnAct, kernel_size=3)
BnActConv = partial(NormActConv, normalization=nn.BatchNorm2d)
ConvBnDropAct = partial(
    ConvNormRegAct, normalization=nn.BatchNorm2d, regularization=nn.Dropout2d
)


from PIL import Image, ImageDraw
import random
import time

en_blue = 240   #0b11110000
en_red = 248    #0b11111000
en_green = 254  #0b11111110
for_encode = [en_red, en_green, en_blue]

dec_blue = 15   #0b00001111
dec_red = 7     #0b00000111
dec_green = 1   #0b00000001
for_decode = [dec_red, dec_green, dec_blue]

MAX_INTERVAL = 100   # максимальный интервал между зашифрованными пикселями

# объединить три части по 4, 3 и 1 биту в целый байт
def join_byte(pixels: list):
    """
    pixels: list -- пиксель
    """
    result = "0b"
    result += bin(pixels[0])[2:].zfill(3)
    result += bin(pixels[1])[2:].zfill(1)
    result += bin(pixels[2])[2:].zfill(4)
    return int(result, 2)

# считать из пикселя разделенный байт
def read_byte(pixel):
    result = []
    for i in range(3):
        result.append(pixel[i] & for_decode[i])
    return result

# выдать последовательность пикселей по сиду
def pixel_generator(rows, columns, seed):
    random.seed(seed)
    last_pixel_in_row = 0
    last_pixel_in_column = 2
    while last_pixel_in_row != rows:
        last_pixel_in_column += random.randint(5, MAX_INTERVAL)
        if last_pixel_in_column >= columns:
            last_pixel_in_column %= columns
            last_pixel_in_row += 1
        if last_pixel_in_row == rows:
            return
        yield (last_pixel_in_column, last_pixel_in_row)

# алгоритм расшифровки
def decrypt(image):
    pixels = image.load()
    image_row = image.size[1]           # высота изображения
    image_column = image.size[0]        # ширина изображения

    # чтение сида
    decrypted = read_byte(pixels[(0, 0)])
    seed = join_byte(decrypted)

    # чтение длины сообщения
    decrypted = read_byte(pixels[(1, 0)])
    bytes_count = join_byte(decrypted)

    # чтение сообщения из изображения
    pixel_gen = pixel_generator(image_row, image_column, seed)
    bytes_list = [] # список байт сообщения
    for _ in range(bytes_count):
        pixel = next(pixel_gen)
        decrypted = read_byte(pixels[pixel])
        decrypted = join_byte(decrypted)
        bytes_list.append(decrypted)
    return bytes(bytes_list).decode('utf-8')


#!/usr/bin/env python3.8
def fizzbuzz(b):
    for i in range(1,b+1):
        if i%5==0 and i%3==0:
            print("FizzBuzz", i)
        elif i%3==0:
            print("Fizz", i)
        elif i%5==0:
            print('Buzz', i)
        else:
            pass
#fizzbuzz(30)
def fizz(b):
    for i in range(1,b+1):
        strn = ''
        if i%3==0:
            strn+='Fizz'
        if i%5==0:
            strn+='Buzz'
        if len(strn)>=1:
            print(strn, i)
        strn=''
fizz(30)


#practical 8
#table based on lecture note - i cannot make it square after  hours of research
#ask for table size
#set coutner for i
#while loop until i reaches table size
#set j counter and print i*j until table size reached with increments in 1

table_size=int(input('Enter desired size of multiplication table: '))

i=1
while i<= table_size:
    j=1
    while j<=i:
        print(i*j,'',end='')
        j+=1
    print()
    i+=1