diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..22eee2d --- /dev/null +++ b/404.html @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + pulse-eco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/PulseEcoAPI/index.html b/PulseEcoAPI/index.html new file mode 100644 index 0000000..b176eaa --- /dev/null +++ b/PulseEcoAPI/index.html @@ -0,0 +1,1971 @@ + + + + + + + + + + + + + + + + + + + + PulseEcoAPI - pulse-eco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + +

PulseEcoAPI

+ + +
+ + + + +
+ + + +
+ + + + + + + + +
+ + + +

+ PulseEcoAPI + + +

+ + +
+

+ Bases: PulseEcoAPIBase

+ + +

Low level unsafe pulse.eco API wrapper.

+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
class PulseEcoAPI(PulseEcoAPIBase):
+    """Low level unsafe pulse.eco API wrapper."""
+
+    def __init__(
+        self,
+        city_name: str,
+        auth: tuple[str, str] | None = None,
+        base_url: str = PULSE_ECO_BASE_URL_FORMAT,
+        session: requests.Session | None = None,
+    ) -> None:
+        """Initialize the pulse.eco API wrapper.
+
+        :param city_name: the city name
+        :param auth: a tuple of (email, password), defaults to None
+        :param base_url: the base URL of the API, defaults to
+            'https://{city_name}.pulse.eco/rest/{end_point}'
+        :param session: a requests session
+            , use this to customize the session and add retries, defaults to None
+        """
+        self.city_name = city_name
+
+        if base_url is not None and PULSE_ECO_BASE_URL_FORMAT_ENV_KEY in os.environ:
+            base_url = os.environ[PULSE_ECO_BASE_URL_FORMAT_ENV_KEY]
+
+        if session is not None:
+            self._session = session
+        else:
+            self._session = requests.Session()
+
+        if auth is None:
+            auth = get_auth_from_env(city_name=city_name)
+
+        if auth is not None:
+            self._session.auth = auth
+
+        self._base_url = base_url
+
+    def _base_request(
+        self, end_point: str, params: dict[str, Any] | None = None
+    ) -> Any:  # noqa: ANN401
+        """Make a request to the PulseEco API.
+
+        :param end_point: an end point of the API
+        :param params: get parameters, defaults to None
+        :return: the response json
+        """
+        if params is None:
+            params = {}
+        url = self._base_url.format(city_name=self.city_name, end_point=end_point)
+        response = self._session.get(url, params=params)
+        response.raise_for_status()
+        return response.json()
+
+    def sensors(self) -> list[Sensor]:
+        """Get all sensors for a city.
+
+        :return: a list of sensors
+        """
+        return cast("list[Sensor]", self._base_request("sensor"))
+
+    def sensor(self, sensor_id: str) -> Sensor:
+        """Get a sensor by it's ID
+
+        :param sensor_id: the unique ID of the sensor
+        :return: a sensor
+        """
+        return cast(Sensor, self._base_request(f"sensor/{sensor_id}"))
+
+    def data_raw(
+        self,
+        from_: str | datetime.datetime,
+        to: str | datetime.datetime,
+        type: str | None = None,
+        sensor_id: str | None = None,
+    ) -> list[DataValueRaw]:
+        """Get raw data for a city.
+
+        :param from_: the start datetime of the data
+            as a datetime object or an isoformat string
+        :param to: the end datetime of the data
+            as a datetime object or an isoformat string
+        :param type: the data value type, defaults to None
+        :param sensor_id: the unique ID of the sensor, defaults to None
+        :return: a list of data values
+        """
+        if sensor_id is None and type is None:
+            warnings.warn(
+                "Warning! If you encounter an error, "
+                "you should probably specify either sensor_id or type.",
+                stacklevel=2,
+            )
+        data: list[DataValueRaw] = []
+        datetime_spans = split_datetime_span(from_, to, DATA_RAW_MAX_SPAN)
+        for from_temp, to_temp in datetime_spans:
+            params = {
+                "sensorId": sensor_id,
+                "type": type,
+                "from": convert_datetime_to_str(from_temp),
+                "to": convert_datetime_to_str(to_temp),
+            }
+            params = {k: v for k, v in params.items() if v is not None}
+            data_value = cast(
+                "list[DataValueRaw]",
+                self._base_request("dataRaw", params=params),
+            )
+            data += data_value
+        return data
+
+    def avg_data(
+        self,
+        period: str,
+        from_: str | datetime.datetime,
+        to: str | datetime.datetime,
+        type: str,
+        sensor_id: str | None = None,
+    ) -> list[DataValueAvg]:
+        """Get average data for a city.
+
+        :param period: the period of the average data (day, week, month)
+        :param from_: the start datetime of the data
+            as a datetime object or an isoformat string
+        :param to: the end datetime of the data
+            as a datetime object or an isoformat string
+        :param type: the data value type
+        :param sensor_id: the unique ID of the sensor, defaults to None
+        :return: a list of average data values
+        """
+        if period not in {"day", "week", "month"}:
+            warnings.warn(
+                "Warning! Invalid value for period. "
+                "Should be one of: day, week, month",
+                stacklevel=2,
+            )
+        data: list[DataValueAvg] = []
+        datetime_spans = split_datetime_span(from_, to, AVG_DATA_MAX_SPAN)
+        for from_temp, to_temp in datetime_spans:
+            params = {
+                "sensorId": sensor_id,
+                "type": type,
+                "from": convert_datetime_to_str(from_temp),
+                "to": convert_datetime_to_str(to_temp),
+            }
+            params = {k: v for k, v in params.items() if v is not None}
+            data_value = cast(
+                "list[DataValueAvg]",
+                self._base_request(f"avgData/{period}", params=params),
+            )
+            data += data_value
+        return data
+
+    def data24h(self) -> list[DataValueRaw]:
+        """Get 24h data for a city.
+
+        The data values are sorted ascending by their timestamp.
+
+        :return: a list of data values for the past 24 hours
+        """
+        return cast("list[DataValueRaw]", self._base_request("data24h"))
+
+    def current(self) -> list[DataValueRaw]:
+        """Get the last received valid data for each sensor in a city
+
+        Will not return sensor data older than 2 hours.
+
+        :return: a list of current data values
+        """
+        return cast("list[DataValueRaw]", self._base_request("current"))
+
+    def overall(
+        self,
+    ) -> Overall:
+        """Get the current average data for all sensors per value for a city.
+
+        ## Example:
+
+        ```python
+        {
+            'cityName': 'skopje',
+            'values': {
+                'no2': '22',
+                'o3': '4',
+                'pm25': '53',
+                'pm10': '73',
+                'temperature': '7',
+                'humidity': '71',
+                'pressure': '992',
+                'noise_dba': '43'
+            }
+        }
+        ```
+
+        :return: the overall data for the city
+        """
+        return cast(Overall, self._base_request("overall"))
+
+
+ + + +
+ + + + + + + + + + +
+ + + +

+ __init__(city_name, auth=None, base_url=PULSE_ECO_BASE_URL_FORMAT, session=None) + +

+ + +
+ +

Initialize the pulse.eco API wrapper.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
city_name + str + +
+

the city name

+
+
+ required +
auth + tuple[str, str] | None + +
+

a tuple of (email, password), defaults to None

+
+
+ None +
base_url + str + +
+

the base URL of the API, defaults to 'https://{city_name}.pulse.eco/rest/{end_point}'

+
+
+ PULSE_ECO_BASE_URL_FORMAT +
session + Session | None + +
+

a requests session , use this to customize the session and add retries, defaults to None

+
+
+ None +
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def __init__(
+    self,
+    city_name: str,
+    auth: tuple[str, str] | None = None,
+    base_url: str = PULSE_ECO_BASE_URL_FORMAT,
+    session: requests.Session | None = None,
+) -> None:
+    """Initialize the pulse.eco API wrapper.
+
+    :param city_name: the city name
+    :param auth: a tuple of (email, password), defaults to None
+    :param base_url: the base URL of the API, defaults to
+        'https://{city_name}.pulse.eco/rest/{end_point}'
+    :param session: a requests session
+        , use this to customize the session and add retries, defaults to None
+    """
+    self.city_name = city_name
+
+    if base_url is not None and PULSE_ECO_BASE_URL_FORMAT_ENV_KEY in os.environ:
+        base_url = os.environ[PULSE_ECO_BASE_URL_FORMAT_ENV_KEY]
+
+    if session is not None:
+        self._session = session
+    else:
+        self._session = requests.Session()
+
+    if auth is None:
+        auth = get_auth_from_env(city_name=city_name)
+
+    if auth is not None:
+        self._session.auth = auth
+
+    self._base_url = base_url
+
+
+
+ +
+ + +
+ + + +

+ avg_data(period, from_, to, type, sensor_id=None) + +

+ + +
+ +

Get average data for a city.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
period + str + +
+

the period of the average data (day, week, month)

+
+
+ required +
from_ + str | datetime + +
+

the start datetime of the data as a datetime object or an isoformat string

+
+
+ required +
to + str | datetime + +
+

the end datetime of the data as a datetime object or an isoformat string

+
+
+ required +
type + str + +
+

the data value type

+
+
+ required +
sensor_id + str | None + +
+

the unique ID of the sensor, defaults to None

+
+
+ None +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValueAvg] + +
+

a list of average data values

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def avg_data(
+    self,
+    period: str,
+    from_: str | datetime.datetime,
+    to: str | datetime.datetime,
+    type: str,
+    sensor_id: str | None = None,
+) -> list[DataValueAvg]:
+    """Get average data for a city.
+
+    :param period: the period of the average data (day, week, month)
+    :param from_: the start datetime of the data
+        as a datetime object or an isoformat string
+    :param to: the end datetime of the data
+        as a datetime object or an isoformat string
+    :param type: the data value type
+    :param sensor_id: the unique ID of the sensor, defaults to None
+    :return: a list of average data values
+    """
+    if period not in {"day", "week", "month"}:
+        warnings.warn(
+            "Warning! Invalid value for period. "
+            "Should be one of: day, week, month",
+            stacklevel=2,
+        )
+    data: list[DataValueAvg] = []
+    datetime_spans = split_datetime_span(from_, to, AVG_DATA_MAX_SPAN)
+    for from_temp, to_temp in datetime_spans:
+        params = {
+            "sensorId": sensor_id,
+            "type": type,
+            "from": convert_datetime_to_str(from_temp),
+            "to": convert_datetime_to_str(to_temp),
+        }
+        params = {k: v for k, v in params.items() if v is not None}
+        data_value = cast(
+            "list[DataValueAvg]",
+            self._base_request(f"avgData/{period}", params=params),
+        )
+        data += data_value
+    return data
+
+
+
+ +
+ + +
+ + + +

+ current() + +

+ + +
+ +

Get the last received valid data for each sensor in a city

+

Will not return sensor data older than 2 hours.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValueRaw] + +
+

a list of current data values

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def current(self) -> list[DataValueRaw]:
+    """Get the last received valid data for each sensor in a city
+
+    Will not return sensor data older than 2 hours.
+
+    :return: a list of current data values
+    """
+    return cast("list[DataValueRaw]", self._base_request("current"))
+
+
+
+ +
+ + +
+ + + +

+ data24h() + +

+ + +
+ +

Get 24h data for a city.

+

The data values are sorted ascending by their timestamp.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValueRaw] + +
+

a list of data values for the past 24 hours

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def data24h(self) -> list[DataValueRaw]:
+    """Get 24h data for a city.
+
+    The data values are sorted ascending by their timestamp.
+
+    :return: a list of data values for the past 24 hours
+    """
+    return cast("list[DataValueRaw]", self._base_request("data24h"))
+
+
+
+ +
+ + +
+ + + +

+ data_raw(from_, to, type=None, sensor_id=None) + +

+ + +
+ +

Get raw data for a city.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
from_ + str | datetime + +
+

the start datetime of the data as a datetime object or an isoformat string

+
+
+ required +
to + str | datetime + +
+

the end datetime of the data as a datetime object or an isoformat string

+
+
+ required +
type + str | None + +
+

the data value type, defaults to None

+
+
+ None +
sensor_id + str | None + +
+

the unique ID of the sensor, defaults to None

+
+
+ None +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValueRaw] + +
+

a list of data values

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def data_raw(
+    self,
+    from_: str | datetime.datetime,
+    to: str | datetime.datetime,
+    type: str | None = None,
+    sensor_id: str | None = None,
+) -> list[DataValueRaw]:
+    """Get raw data for a city.
+
+    :param from_: the start datetime of the data
+        as a datetime object or an isoformat string
+    :param to: the end datetime of the data
+        as a datetime object or an isoformat string
+    :param type: the data value type, defaults to None
+    :param sensor_id: the unique ID of the sensor, defaults to None
+    :return: a list of data values
+    """
+    if sensor_id is None and type is None:
+        warnings.warn(
+            "Warning! If you encounter an error, "
+            "you should probably specify either sensor_id or type.",
+            stacklevel=2,
+        )
+    data: list[DataValueRaw] = []
+    datetime_spans = split_datetime_span(from_, to, DATA_RAW_MAX_SPAN)
+    for from_temp, to_temp in datetime_spans:
+        params = {
+            "sensorId": sensor_id,
+            "type": type,
+            "from": convert_datetime_to_str(from_temp),
+            "to": convert_datetime_to_str(to_temp),
+        }
+        params = {k: v for k, v in params.items() if v is not None}
+        data_value = cast(
+            "list[DataValueRaw]",
+            self._base_request("dataRaw", params=params),
+        )
+        data += data_value
+    return data
+
+
+
+ +
+ + +
+ + + +

+ overall() + +

+ + +
+ +

Get the current average data for all sensors per value for a city.

+
Example:
+
{
+    'cityName': 'skopje',
+    'values': {
+        'no2': '22',
+        'o3': '4',
+        'pm25': '53',
+        'pm10': '73',
+        'temperature': '7',
+        'humidity': '71',
+        'pressure': '992',
+        'noise_dba': '43'
+    }
+}
+
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Overall + +
+

the overall data for the city

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def overall(
+    self,
+) -> Overall:
+    """Get the current average data for all sensors per value for a city.
+
+    ## Example:
+
+    ```python
+    {
+        'cityName': 'skopje',
+        'values': {
+            'no2': '22',
+            'o3': '4',
+            'pm25': '53',
+            'pm10': '73',
+            'temperature': '7',
+            'humidity': '71',
+            'pressure': '992',
+            'noise_dba': '43'
+        }
+    }
+    ```
+
+    :return: the overall data for the city
+    """
+    return cast(Overall, self._base_request("overall"))
+
+
+
+ +
+ + +
+ + + +

+ sensor(sensor_id) + +

+ + +
+ +

Get a sensor by it's ID

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
sensor_id + str + +
+

the unique ID of the sensor

+
+
+ required +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Sensor + +
+

a sensor

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def sensor(self, sensor_id: str) -> Sensor:
+    """Get a sensor by it's ID
+
+    :param sensor_id: the unique ID of the sensor
+    :return: a sensor
+    """
+    return cast(Sensor, self._base_request(f"sensor/{sensor_id}"))
+
+
+
+ +
+ + +
+ + + +

+ sensors() + +

+ + +
+ +

Get all sensors for a city.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[Sensor] + +
+

a list of sensors

+
+
+ +
+ Source code in pulseeco/api/pulse_eco_api.py +
def sensors(self) -> list[Sensor]:
+    """Get all sensors for a city.
+
+    :return: a list of sensors
+    """
+    return cast("list[Sensor]", self._base_request("sensor"))
+
+
+
+ +
+ + + +
+ +
+ + +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/PulseEcoClient/index.html b/PulseEcoClient/index.html new file mode 100644 index 0000000..889f2a7 --- /dev/null +++ b/PulseEcoClient/index.html @@ -0,0 +1,1736 @@ + + + + + + + + + + + + + + + + + + + + + + PulseEcoClient - pulse-eco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + +

PulseEcoClient

+ + +
+ + + + +
+ + + +
+ + + + + + + + +
+ + + +

+ PulseEcoClient + + +

+ + +
+ + +

High level pulse.eco client.

+ +
+ Source code in pulseeco/client.py +
class PulseEcoClient:
+    """High level pulse.eco client."""
+
+    def __init__(
+        self,
+        city_name: str,
+        auth: tuple[str, str] | None = None,
+        base_url: str = PULSE_ECO_BASE_URL_FORMAT,
+        session: requests.Session | None = None,
+        pulse_eco_api: PulseEcoAPIBase | None = None,
+    ) -> None:
+        """Initialize the pulse.eco client.
+
+        :param city_name: the city name
+        :param auth: a tuple of (email, password), defaults to None
+        :param base_url: the base URL of the API, defaults to
+            'https://{city_name}.pulse.eco/rest/{end_point}'
+        :param session: a requests session
+            use this to customize the session and add retries, defaults to None,
+        :param pulse_eco_api: a pulse.eco API wrapper, defaults to None,
+            if set, the other parameters are ignored
+        """
+        self._pulse_eco_api: PulseEcoAPIBase
+        if pulse_eco_api is None:
+            self._pulse_eco_api = PulseEcoAPI(
+                city_name=city_name, auth=auth, base_url=base_url, session=session
+            )
+        else:
+            self._pulse_eco_api = pulse_eco_api
+
+    def sensors(self) -> list[Sensor]:
+        """Get all sensors for a city.
+
+        :return: a list of sensors
+        """
+        return [
+            Sensor.model_validate(sensor) for sensor in self._pulse_eco_api.sensors()
+        ]
+
+    def sensor(self, sensor_id: str) -> Sensor:
+        """Get a sensor by it's ID.
+
+        :param sensor_id: the unique ID of the sensor
+        :return: a sensor
+        """
+        return Sensor.model_validate(self._pulse_eco_api.sensor(sensor_id=sensor_id))
+
+    def data_raw(
+        self,
+        from_: str | datetime.datetime,
+        to: str | datetime.datetime,
+        type: DataValueType | None = None,
+        sensor_id: str | None = None,
+    ) -> list[DataValue]:
+        """Get raw data for a city.
+
+        :param from_: the start datetime of the data
+            as a datetime object or an isoformat string
+        :param to: the end datetime of the data
+            as a datetime object or an isoformat string
+        :param type: the data value type, defaults to None
+        :param sensor_id: the unique ID of the sensor, defaults to None
+        :return: a list of data values
+        """
+        return [
+            DataValue.model_validate(data_value)
+            for data_value in self._pulse_eco_api.data_raw(
+                from_=from_,
+                to=to,
+                type=type,
+                sensor_id=sensor_id,
+            )
+        ]
+
+    def avg_data(
+        self,
+        period: AveragePeriod,
+        from_: str | datetime.datetime,
+        to: str | datetime.datetime,
+        type: DataValueType,
+        sensor_id: str | None = None,
+    ) -> list[DataValue]:
+        """Get average data for a city.
+
+        :param period: the period of the average data
+        :param from_: the start datetime of the data
+            as a datetime object or an isoformat string
+        :param to: the end datetime of the data
+            as a datetime object or an isoformat string
+        :param type: the data value type
+        :param sensor_id: the unique ID of the sensor, defaults to None
+        :return: a list of average data values
+        """
+        return [
+            DataValue.model_validate(data_value)
+            for data_value in self._pulse_eco_api.avg_data(
+                period=period,
+                from_=from_,
+                to=to,
+                type=type,
+                sensor_id=sensor_id,
+            )
+        ]
+
+    def data24h(self) -> list[DataValue]:
+        """Get 24h data for a city.
+
+        The data values are sorted ascending by their timestamp.
+
+        :return: a list of data values for the past 24 hours
+        """
+        return [
+            DataValue.model_validate(data_value)
+            for data_value in self._pulse_eco_api.data24h()
+        ]
+
+    def current(self) -> list[DataValue]:
+        """Get the last received valid data for each sensor in a city.
+
+        Will not return sensor data older than 2 hours.
+
+        :return: a list of current data values
+        """
+        return [
+            DataValue.model_validate(data_value)
+            for data_value in self._pulse_eco_api.current()
+        ]
+
+    def overall(self) -> Overall:
+        """Get the current average data for all sensors per value for a city.
+
+        :return: the overall data for the city
+        """
+        return Overall.model_validate(self._pulse_eco_api.overall())
+
+
+ + + +
+ + + + + + + + + + +
+ + + +

+ __init__(city_name, auth=None, base_url=PULSE_ECO_BASE_URL_FORMAT, session=None, pulse_eco_api=None) + +

+ + +
+ +

Initialize the pulse.eco client.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
city_name + str + +
+

the city name

+
+
+ required +
auth + tuple[str, str] | None + +
+

a tuple of (email, password), defaults to None

+
+
+ None +
base_url + str + +
+

the base URL of the API, defaults to 'https://{city_name}.pulse.eco/rest/{end_point}'

+
+
+ PULSE_ECO_BASE_URL_FORMAT +
session + Session | None + +
+

a requests session use this to customize the session and add retries, defaults to None,

+
+
+ None +
pulse_eco_api + PulseEcoAPIBase | None + +
+

a pulse.eco API wrapper, defaults to None, if set, the other parameters are ignored

+
+
+ None +
+ +
+ Source code in pulseeco/client.py +
21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
def __init__(
+    self,
+    city_name: str,
+    auth: tuple[str, str] | None = None,
+    base_url: str = PULSE_ECO_BASE_URL_FORMAT,
+    session: requests.Session | None = None,
+    pulse_eco_api: PulseEcoAPIBase | None = None,
+) -> None:
+    """Initialize the pulse.eco client.
+
+    :param city_name: the city name
+    :param auth: a tuple of (email, password), defaults to None
+    :param base_url: the base URL of the API, defaults to
+        'https://{city_name}.pulse.eco/rest/{end_point}'
+    :param session: a requests session
+        use this to customize the session and add retries, defaults to None,
+    :param pulse_eco_api: a pulse.eco API wrapper, defaults to None,
+        if set, the other parameters are ignored
+    """
+    self._pulse_eco_api: PulseEcoAPIBase
+    if pulse_eco_api is None:
+        self._pulse_eco_api = PulseEcoAPI(
+            city_name=city_name, auth=auth, base_url=base_url, session=session
+        )
+    else:
+        self._pulse_eco_api = pulse_eco_api
+
+
+
+ +
+ + +
+ + + +

+ avg_data(period, from_, to, type, sensor_id=None) + +

+ + +
+ +

Get average data for a city.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
period + AveragePeriod + +
+

the period of the average data

+
+
+ required +
from_ + str | datetime + +
+

the start datetime of the data as a datetime object or an isoformat string

+
+
+ required +
to + str | datetime + +
+

the end datetime of the data as a datetime object or an isoformat string

+
+
+ required +
type + DataValueType + +
+

the data value type

+
+
+ required +
sensor_id + str | None + +
+

the unique ID of the sensor, defaults to None

+
+
+ None +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValue] + +
+

a list of average data values

+
+
+ +
+ Source code in pulseeco/client.py +
def avg_data(
+    self,
+    period: AveragePeriod,
+    from_: str | datetime.datetime,
+    to: str | datetime.datetime,
+    type: DataValueType,
+    sensor_id: str | None = None,
+) -> list[DataValue]:
+    """Get average data for a city.
+
+    :param period: the period of the average data
+    :param from_: the start datetime of the data
+        as a datetime object or an isoformat string
+    :param to: the end datetime of the data
+        as a datetime object or an isoformat string
+    :param type: the data value type
+    :param sensor_id: the unique ID of the sensor, defaults to None
+    :return: a list of average data values
+    """
+    return [
+        DataValue.model_validate(data_value)
+        for data_value in self._pulse_eco_api.avg_data(
+            period=period,
+            from_=from_,
+            to=to,
+            type=type,
+            sensor_id=sensor_id,
+        )
+    ]
+
+
+
+ +
+ + +
+ + + +

+ current() + +

+ + +
+ +

Get the last received valid data for each sensor in a city.

+

Will not return sensor data older than 2 hours.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValue] + +
+

a list of current data values

+
+
+ +
+ Source code in pulseeco/client.py +
def current(self) -> list[DataValue]:
+    """Get the last received valid data for each sensor in a city.
+
+    Will not return sensor data older than 2 hours.
+
+    :return: a list of current data values
+    """
+    return [
+        DataValue.model_validate(data_value)
+        for data_value in self._pulse_eco_api.current()
+    ]
+
+
+
+ +
+ + +
+ + + +

+ data24h() + +

+ + +
+ +

Get 24h data for a city.

+

The data values are sorted ascending by their timestamp.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValue] + +
+

a list of data values for the past 24 hours

+
+
+ +
+ Source code in pulseeco/client.py +
def data24h(self) -> list[DataValue]:
+    """Get 24h data for a city.
+
+    The data values are sorted ascending by their timestamp.
+
+    :return: a list of data values for the past 24 hours
+    """
+    return [
+        DataValue.model_validate(data_value)
+        for data_value in self._pulse_eco_api.data24h()
+    ]
+
+
+
+ +
+ + +
+ + + +

+ data_raw(from_, to, type=None, sensor_id=None) + +

+ + +
+ +

Get raw data for a city.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
from_ + str | datetime + +
+

the start datetime of the data as a datetime object or an isoformat string

+
+
+ required +
to + str | datetime + +
+

the end datetime of the data as a datetime object or an isoformat string

+
+
+ required +
type + DataValueType | None + +
+

the data value type, defaults to None

+
+
+ None +
sensor_id + str | None + +
+

the unique ID of the sensor, defaults to None

+
+
+ None +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[DataValue] + +
+

a list of data values

+
+
+ +
+ Source code in pulseeco/client.py +
65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
def data_raw(
+    self,
+    from_: str | datetime.datetime,
+    to: str | datetime.datetime,
+    type: DataValueType | None = None,
+    sensor_id: str | None = None,
+) -> list[DataValue]:
+    """Get raw data for a city.
+
+    :param from_: the start datetime of the data
+        as a datetime object or an isoformat string
+    :param to: the end datetime of the data
+        as a datetime object or an isoformat string
+    :param type: the data value type, defaults to None
+    :param sensor_id: the unique ID of the sensor, defaults to None
+    :return: a list of data values
+    """
+    return [
+        DataValue.model_validate(data_value)
+        for data_value in self._pulse_eco_api.data_raw(
+            from_=from_,
+            to=to,
+            type=type,
+            sensor_id=sensor_id,
+        )
+    ]
+
+
+
+ +
+ + +
+ + + +

+ overall() + +

+ + +
+ +

Get the current average data for all sensors per value for a city.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Overall + +
+

the overall data for the city

+
+
+ +
+ Source code in pulseeco/client.py +
def overall(self) -> Overall:
+    """Get the current average data for all sensors per value for a city.
+
+    :return: the overall data for the city
+    """
+    return Overall.model_validate(self._pulse_eco_api.overall())
+
+
+
+ +
+ + +
+ + + +

+ sensor(sensor_id) + +

+ + +
+ +

Get a sensor by it's ID.

+ + + +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
sensor_id + str + +
+

the unique ID of the sensor

+
+
+ required +
+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ Sensor + +
+

a sensor

+
+
+ +
+ Source code in pulseeco/client.py +
57
+58
+59
+60
+61
+62
+63
def sensor(self, sensor_id: str) -> Sensor:
+    """Get a sensor by it's ID.
+
+    :param sensor_id: the unique ID of the sensor
+    :return: a sensor
+    """
+    return Sensor.model_validate(self._pulse_eco_api.sensor(sensor_id=sensor_id))
+
+
+
+ +
+ + +
+ + + +

+ sensors() + +

+ + +
+ +

Get all sensors for a city.

+ + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list[Sensor] + +
+

a list of sensors

+
+
+ +
+ Source code in pulseeco/client.py +
48
+49
+50
+51
+52
+53
+54
+55
def sensors(self) -> list[Sensor]:
+    """Get all sensors for a city.
+
+    :return: a list of sensors
+    """
+    return [
+        Sensor.model_validate(sensor) for sensor in self._pulse_eco_api.sensors()
+    ]
+
+
+
+ +
+ + + +
+ +
+ + +
+ + + + +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 0000000..4b7d98b --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,109 @@ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +/* Symbols in Navigation and ToC. */ +:root, +[data-md-color-scheme="default"] { + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} \ No newline at end of file diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000..1cf13b9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.51d95adb.min.js b/assets/javascripts/bundle.51d95adb.min.js new file mode 100644 index 0000000..b20ec68 --- /dev/null +++ b/assets/javascripts/bundle.51d95adb.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Hi=Object.create;var xr=Object.defineProperty;var Pi=Object.getOwnPropertyDescriptor;var $i=Object.getOwnPropertyNames,kt=Object.getOwnPropertySymbols,Ii=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&on(e,r,t[r]);if(kt)for(var r of kt(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&kt)for(var n of kt(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var Ht=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Fi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $i(t))!Er.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=Pi(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Hi(Ii(e)):{},Fi(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var fn=Ht((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(T){return!!(T&&T!==document&&T.nodeName!=="HTML"&&T.nodeName!=="BODY"&&"classList"in T&&"contains"in T.classList)}function f(T){var Ke=T.type,We=T.tagName;return!!(We==="INPUT"&&a[Ke]&&!T.readOnly||We==="TEXTAREA"&&!T.readOnly||T.isContentEditable)}function c(T){T.classList.contains("focus-visible")||(T.classList.add("focus-visible"),T.setAttribute("data-focus-visible-added",""))}function u(T){T.hasAttribute("data-focus-visible-added")&&(T.classList.remove("focus-visible"),T.removeAttribute("data-focus-visible-added"))}function p(T){T.metaKey||T.altKey||T.ctrlKey||(s(r.activeElement)&&c(r.activeElement),n=!0)}function m(T){n=!1}function d(T){s(T.target)&&(n||f(T.target))&&c(T.target)}function h(T){s(T.target)&&(T.target.classList.contains("focus-visible")||T.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(T.target))}function v(T){document.visibilityState==="hidden"&&(o&&(n=!0),B())}function B(){document.addEventListener("mousemove",z),document.addEventListener("mousedown",z),document.addEventListener("mouseup",z),document.addEventListener("pointermove",z),document.addEventListener("pointerdown",z),document.addEventListener("pointerup",z),document.addEventListener("touchmove",z),document.addEventListener("touchstart",z),document.addEventListener("touchend",z)}function re(){document.removeEventListener("mousemove",z),document.removeEventListener("mousedown",z),document.removeEventListener("mouseup",z),document.removeEventListener("pointermove",z),document.removeEventListener("pointerdown",z),document.removeEventListener("pointerup",z),document.removeEventListener("touchmove",z),document.removeEventListener("touchstart",z),document.removeEventListener("touchend",z)}function z(T){T.target.nodeName&&T.target.nodeName.toLowerCase()==="html"||(n=!1,re())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),B(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var un=Ht(Sr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},a=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(re,z){d.append(z,re)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(T){throw new Error("URL unable to set base "+c+" due to "+T)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,B=!0,re=this;["append","delete","set"].forEach(function(T){var Ke=h[T];h[T]=function(){Ke.apply(h,arguments),v&&(B=!1,re.search=h.toString(),B=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var z=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==z&&(z=this.search,B&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},a=i.prototype,s=function(f){Object.defineProperty(a,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){s(f)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr)});var Qr=Ht((Lt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ki}});var a=i(279),s=i.n(a),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(O){return!1}}var d=function(O){var w=p()(O);return m("cut"),w},h=d;function v(j){var O=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[O?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(k,"px"),w.setAttribute("readonly",""),w.value=j,w}var B=function(O,w){var k=v(O);w.container.appendChild(k);var F=p()(k);return m("copy"),k.remove(),F},re=function(O){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof O=="string"?k=B(O,w):O instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(O==null?void 0:O.type)?k=B(O.value,w):(k=p()(O),m("copy")),k},z=re;function T(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?T=function(w){return typeof w}:T=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},T(j)}var Ke=function(){var O=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=O.action,k=w===void 0?"copy":w,F=O.container,q=O.target,Le=O.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&T(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Le)return z(Le,{container:F});if(q)return k==="cut"?h(q):z(q,{container:F})},We=Ke;function Ie(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(w){return typeof w}:Ie=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},Ie(j)}function Ti(j,O){if(!(j instanceof O))throw new TypeError("Cannot call a class as a function")}function nn(j,O){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof F.action=="function"?F.action:this.defaultAction,this.target=typeof F.target=="function"?F.target:this.defaultTarget,this.text=typeof F.text=="function"?F.text:this.defaultText,this.container=Ie(F.container)==="object"?F.container:document.body}},{key:"listenClick",value:function(F){var q=this;this.listener=c()(F,"click",function(Le){return q.onClick(Le)})}},{key:"onClick",value:function(F){var q=F.delegateTarget||F.currentTarget,Le=this.action(q)||"copy",Rt=We({action:Le,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Rt?"success":"error",{action:Le,text:Rt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(F){return yr("action",F)}},{key:"defaultTarget",value:function(F){var q=yr("target",F);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(F){return yr("text",F)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(F){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return z(F,q)}},{key:"cut",value:function(F){return h(F)}},{key:"isSupported",value:function(){var F=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof F=="string"?[F]:F,Le=!!document.queryCommandSupported;return q.forEach(function(Rt){Le=Le&&!!document.queryCommandSupported(Rt)}),Le}}]),w}(s()),ki=Ri},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,f){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(f))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof m=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(m))return c(m,d,h);if(a.nodeList(m))return u(m,d,h);if(a.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return s(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),a=f.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var f=this;function c(){f.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=s.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var is=/["'&<>]/;Jo.exports=as;function as(e){var t=""+e,r=is.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||s(m,d)})})}function s(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof Xe?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){s("next",m)}function u(m){s("throw",m)}function p(m,d){m(d),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof xe=="function"?xe(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,f){a=e[i](a),o(s,f,a.done,a.value)})}}function o(i,a,s,f){Promise.resolve(f).then(function(c){i({value:c,done:s})},a)}}function A(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var $t=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function De(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=xe(a),f=s.next();!f.done;f=s.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(A(u))try{u()}catch(v){i=v instanceof $t?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=xe(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{dn(h)}catch(v){i=i!=null?i:[],v instanceof $t?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new $t(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)dn(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&De(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&De(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Or=Fe.EMPTY;function It(e){return e instanceof Fe||e&&"closed"in e&&A(e.remove)&&A(e.add)&&A(e.unsubscribe)}function dn(e){A(e)?e():e.unsubscribe()}var Ae={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Or:(this.currentObservers=null,s.push(r),new Fe(function(){n.currentObservers=null,De(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new wn(r,n)},t}(U);var wn=function(e){ne(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Or},t}(E);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ne(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,f=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Ut);var On=function(e){ne(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Wt);var we=new On(Tn);var R=new U(function(e){return e.complete()});function Dt(e){return e&&A(e.schedule)}function kr(e){return e[e.length-1]}function Qe(e){return A(kr(e))?e.pop():void 0}function Se(e){return Dt(kr(e))?e.pop():void 0}function Vt(e,t){return typeof kr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function zt(e){return A(e==null?void 0:e.then)}function Nt(e){return A(e[ft])}function qt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Kt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qt=Ki();function Yt(e){return A(e==null?void 0:e[Qt])}function Gt(e){return ln(this,arguments,function(){var r,n,o,i;return Pt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Bt(e){return A(e==null?void 0:e.getReader)}function $(e){if(e instanceof U)return e;if(e!=null){if(Nt(e))return Qi(e);if(pt(e))return Yi(e);if(zt(e))return Gi(e);if(qt(e))return _n(e);if(Yt(e))return Bi(e);if(Bt(e))return Ji(e)}throw Kt(e)}function Qi(e){return new U(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Yi(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?_(function(o,i){return e(o,i,n)}):me,Oe(1),r?He(t):zn(function(){return new Xt}))}}function Nn(){for(var e=[],t=0;t=2,!0))}function fe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new E}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,f=s===void 0?!0:s;return function(c){var u,p,m,d=0,h=!1,v=!1,B=function(){p==null||p.unsubscribe(),p=void 0},re=function(){B(),u=m=void 0,h=v=!1},z=function(){var T=u;re(),T==null||T.unsubscribe()};return g(function(T,Ke){d++,!v&&!h&&B();var We=m=m!=null?m:r();Ke.add(function(){d--,d===0&&!v&&!h&&(p=jr(z,f))}),We.subscribe(Ke),!u&&d>0&&(u=new et({next:function(Ie){return We.next(Ie)},error:function(Ie){v=!0,B(),p=jr(re,o,Ie),We.error(Ie)},complete:function(){h=!0,B(),p=jr(re,a),We.complete()}}),$(T).subscribe(u))})(c)}}function jr(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function V(e,t=document){let r=se(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function se(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),N(e===_e()),Y())}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function Yn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,we),l(()=>Be(e)),N(Be(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,we),l(()=>rr(e)),N(rr(e)))}var Bn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),xa?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ya.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Jn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Zn=typeof WeakMap!="undefined"?new WeakMap:new Bn,eo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ea.getInstance(),n=new Ra(t,r,this);Zn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){eo.prototype[e]=function(){var t;return(t=Zn.get(this))[e].apply(t,arguments)}});var ka=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:eo}(),to=ka;var ro=new E,Ha=I(()=>H(new to(e=>{for(let t of e)ro.next(t)}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return Ha.pipe(S(t=>t.observe(e)),x(t=>ro.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>de(e)))),N(de(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var no=new E,Pa=I(()=>H(new IntersectionObserver(e=>{for(let t of e)no.next(t)},{threshold:0}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function sr(e){return Pa.pipe(S(t=>t.observe(e)),x(t=>no.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function oo(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=de(e),o=bt(e);return r>=o.height-n.height-t}),Y())}var cr={drawer:V("[data-md-toggle=drawer]"),search:V("[data-md-toggle=search]")};function io(e){return cr[e].checked}function qe(e,t){cr[e].checked!==t&&cr[e].click()}function je(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),N(t.checked))}function $a(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ia(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function ao(){let e=b(window,"keydown").pipe(_(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:io("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),_(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!$a(n,r)}return!0}),fe());return Ia().pipe(x(t=>t?R:e))}function Me(){return new URL(location.href)}function ot(e){location.href=e.href}function so(){return new E}function co(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)co(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)co(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function fo(){return location.hash.substring(1)}function uo(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Fa(){return b(window,"hashchange").pipe(l(fo),N(fo()),_(e=>e.length>0),J(1))}function po(){return Fa().pipe(l(e=>se(`[id="${e}"]`)),_(e=>typeof e!="undefined"))}function Nr(e){let t=matchMedia(e);return Zt(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function lo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function qr(e,t){return e.pipe(x(r=>r?t():R))}function ur(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(ce(()=>R),x(r=>r.status!==200?Tt(()=>new Error(r.statusText)):H(r)))}function Ue(e,t){return ur(e,t).pipe(x(r=>r.json()),J(1))}function mo(e,t){let r=new DOMParser;return ur(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function pr(e){let t=M("script",{src:e});return I(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(x(()=>Tt(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),Oe(1))))}function ho(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(ho),N(ho()))}function vo(){return{width:innerWidth,height:innerHeight}}function go(){return b(window,"resize",{passive:!0}).pipe(l(vo),N(vo()))}function yo(){return Q([bo(),go()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Q([n,r]).pipe(l(()=>Be(e)));return Q([r,t,o]).pipe(l(([{height:i},{offset:a,size:s},{x:f,y:c}])=>({offset:{x:a.x-f,y:a.y-c+i},size:s})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(a=>{let s=document.createElement("script");s.src=i,s.onload=a,document.body.appendChild(s)})),Promise.resolve())}var r=class{constructor(n){this.url=n,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.m=a=>{a.source===this.w&&(a.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:a.data})),this.onmessage&&this.onmessage(a))},this.e=(a,s,f,c,u)=>{if(s===this.url.toString()){let p=new ErrorEvent("error",{message:a,filename:s,lineno:f,colno:c,error:u});this.dispatchEvent(p),this.onerror&&this.onerror(p)}};let o=new EventTarget;this.addEventListener=o.addEventListener.bind(o),this.removeEventListener=o.removeEventListener.bind(o),this.dispatchEvent=o.dispatchEvent.bind(o);let i=document.createElement("iframe");i.width=i.height=i.frameBorder="0",document.body.appendChild(this.iframe=i),this.w.document.open(),this.w.document.write(` + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + +

Environment variables

+

Base URL format

+

Environment variable: PULSE_ECO_BASE_URL_FORMAT

+

The default base URL format is https://{city_name}.pulse.eco/rest/{end_point}.

+

Authentication

+

Authentication is not required for fetching data. But if provided, it has to be valid for the city.

+

Credentials can also be provided as environment variables. To provide credentials for a city, use the following format:

+
PULSE_ECO_{city_name}_USERNAME
+PULSE_ECO_{city_name}_PASSWORD
+
+

Example environmtent variables in priority order:

+
PULSE_ECO_SKOPJE_USERNAME
+PULSE_ECO_SKOPJE_PASSWORD
+
+PULSE_ECO_skopje_USERNAME
+PULSE_ECO_skopje_PASSWORD
+
+PULSE_ECO_USERNAME
+PULSE_ECO_PASSWORD
+
+

Only use the generic PULSE_ECO_USERNAME and PULSE_ECO_PASSWORD environment variables +if your application requests data from a single city.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/example-usage/index.html b/example-usage/index.html new file mode 100644 index 0000000..9350d36 --- /dev/null +++ b/example-usage/index.html @@ -0,0 +1,605 @@ + + + + + + + + + + + + + + + + + + + + + + Example usage - pulse-eco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + +

Example usage

+

Initialize client

+

Authentication is not required for fetching data. But if provided, it has to be valid. Authentication is per city.

+
from pulseeco import PulseEcoClient
+
+pulse_eco = PulseEcoClient(city_name="skopje", auth=("user", "pass"))
+
+

Get all sensors

+
>>> pulse_eco.sensors()
+[
+  Sensor(
+    sensor_id='sensor_dev_60237_141',
+    position='42.03900255426,21.40771061182',
+    comments='Imported Sensor.community #60237',
+    type='20004',
+    description='Sensor.community 60237',
+    status='NOT_CLAIMED'
+  ),
+  Sensor(
+    sensor_id='sensor_dev_10699_244',
+    position='41.986,21.452',
+    comments='Imported Sensor.community #10699',
+    type='20004',
+    description='Sensor.community 10699',
+    status='NOT_CLAIMED_UNCONFIRMED'
+  ),
+  Sensor(
+    sensor_id='66710fdc-cdfc-4bbe-93a8-7e796fb8a88d',
+    position='41.995238146587674,21.402708292007443',
+    comments='V1 WiFi sensor in Kozle',
+    type='1',
+    description='Kozle',
+    status='ACTIVE'
+  ),
+  ...
+]
+
+

Get a sensor by id

+
>>> pulse_eco.sensor(sensor_id="1000")
+Sensor(
+  sensor_id='1000',
+  position='41.99249998,21.4236110',
+  comments='MOEPP sensor at Centar',
+  type='0',
+  description='MOEPP Centar',
+  status='ACTIVE'
+)
+
+

Get raw data

+

from_ and to can be either datetime.datetime objects or str in ISO 8601 format.

+
>>> import datetime
+>>> from pulseeco import DataValueType
+>>> pulse_eco.data_raw(
+...   from_=datetime.datetime(year=2017, month=3, day=15, hour=2),
+...   to=datetime.datetime(year=2017, month=4, day=19, hour=12),
+...   type=DataValueType.PM10,
+...   sensor_id="1001",
+... )
+[
+  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 3, 15, 3, 0, 8, tzinfo=TzInfo(+01:00)), type='pm10', position='41.9783,21.47', value=28, year=None),
+  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 3, 15, 4, 0, 8, tzinfo=TzInfo(+01:00)), type='pm10', position='41.9783,21.47', value=55, year=None),
+  ...
+  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 4, 19, 12, 0, 9, tzinfo=TzInfo(+02:00)), type='pm10', position='41.9783,21.47', value=6, year=None),
+  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 4, 19, 13, 0, 9, tzinfo=TzInfo(+02:00)), type='pm10', position='41.9783,21.47', value=31, year=None)
+]
+
+

Get average data

+

sensor_id "-1" is a magic value that gives average values for the whole city.

+
>>> import datetime
+>>> from pulseeco import AveragePeriod, DataValueType
+>>> pulse_eco.avg_data(
+...   period=AveragePeriod.MONTH,
+...   from_=datetime.datetime(year=2019, month=3, day=1, hour=12),
+...   to=datetime.datetime(year=2020, month=5, day=1, hour=12),
+...   type=DataValueType.PM10,
+...   sensor_id="-1",
+... )
+[
+  DataValue(sensor_id='-1', stamp=datetime.datetime(2019, 3, 1, 13, 0, tzinfo=TzInfo(+01:00)), type='pm10', position='', value=29, year=None),
+  DataValue(sensor_id='-1', stamp=datetime.datetime(2019, 4, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=19, year=None),
+  ...
+  DataValue(sensor_id='-1', stamp=datetime.datetime(2020, 4, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=17, year=None),
+  DataValue(sensor_id='-1', stamp=datetime.datetime(2020, 5, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=12, year=None)
+]
+
+

Get 24h data

+
>>> pulse_eco.data24h()
+[ ... ]
+
+

Get current data

+

Get the last received valid data for each sensor in a city.

+
>>> pulse_eco.current()
+[ ... ]
+
+

Get overall data

+

Get the current average data for all sensors per value for a city.

+
>>> pulse_eco.overall()
+Overall(
+  city_name='skopje',
+  values=OverallValues(
+    no2=6,
+    o3=10,
+    so2=None,
+    co=None,
+    pm25=56,
+    pm10=95,
+    temperature=6,
+    humidity=73,
+    pressure=995,
+    noise=None,
+    noise_dba=42,
+    gas_resistance=None
+  )
+)
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..98239dc --- /dev/null +++ b/index.html @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + pulse-eco + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + +

README

+

pulse-eco

+

GitHub Workflow Test +codecov +GitHub Workflow Build

+

PyPI +PyPI - Python Version

+

Ruff +types - Mypy +GitHub license

+

pulse.eco logo

+

pulse.eco client for Python.

+

Installation

+

pulse-eco is avialiable on PyPI:

+
python -m pip install pulse-eco
+
+

Requires Python version 3.8+.

+

Documentation

+

Official pulse.eco REST API documentation can be found on pulse.eco/restapi.

+

API Reference and User Guide for this package is available on GitHub Pages.

+

Requesting data with a larger time range

+

The pulse.eco API limits the maximum time span of data you can get from one request. +For /dataRaw it is one week, while for /avgData it is one year.

+

If the time range is larger than the maximum, the pulse.eco client creates multiple requests to the API and then joins the data together. Be aware of this.

+

Development

+

Install Hatch

+

https://hatch.pypa.io/latest/install/

+

Create dev environment

+

Activate a Python 3.8 environment and run:

+
hatch env create dev
+
+

Install pre-commit hooks

+
hatch run dev:setup
+
+

Create .env file

+

Set auth credentials in .env file:

+
cp .env.example .env
+
+

Before committing

+

This command must pass without errors before committing:

+
hatch run dev:check
+
+

Docs

+

To preview the docs locally, run:

+
hatch run dev:docs-serve
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..9e170b1 Binary files /dev/null and b/objects.inv differ diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 0000000..cc99b8d --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"README","text":""},{"location":"#pulse-eco","title":"pulse-eco","text":"

pulse.eco client for Python.

"},{"location":"#installation","title":"Installation","text":"

pulse-eco is avialiable on PyPI:

python -m pip install pulse-eco\n

Requires Python version 3.8+.

"},{"location":"#documentation","title":"Documentation","text":"

Official pulse.eco REST API documentation can be found on pulse.eco/restapi.

API Reference and User Guide for this package is available on GitHub Pages.

"},{"location":"#requesting-data-with-a-larger-time-range","title":"Requesting data with a larger time range","text":"

The pulse.eco API limits the maximum time span of data you can get from one request. For /dataRaw it is one week, while for /avgData it is one year.

If the time range is larger than the maximum, the pulse.eco client creates multiple requests to the API and then joins the data together. Be aware of this.

"},{"location":"#development","title":"Development","text":""},{"location":"#install-hatch","title":"Install Hatch","text":"

https://hatch.pypa.io/latest/install/

"},{"location":"#create-dev-environment","title":"Create dev environment","text":"

Activate a Python 3.8 environment and run:

hatch env create dev\n
"},{"location":"#install-pre-commit-hooks","title":"Install pre-commit hooks","text":"
hatch run dev:setup\n
"},{"location":"#create-env-file","title":"Create .env file","text":"

Set auth credentials in .env file:

cp .env.example .env\n
"},{"location":"#before-committing","title":"Before committing","text":"

This command must pass without errors before committing:

hatch run dev:check\n
"},{"location":"#docs","title":"Docs","text":"

To preview the docs locally, run:

hatch run dev:docs-serve\n
"},{"location":"PulseEcoAPI/","title":"PulseEcoAPI","text":""},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI","title":"PulseEcoAPI","text":"

Bases: PulseEcoAPIBase

Low level unsafe pulse.eco API wrapper.

Source code in pulseeco/api/pulse_eco_api.py
class PulseEcoAPI(PulseEcoAPIBase):\n\"\"\"Low level unsafe pulse.eco API wrapper.\"\"\"\ndef __init__(\nself,\ncity_name: str,\nauth: tuple[str, str] | None = None,\nbase_url: str = PULSE_ECO_BASE_URL_FORMAT,\nsession: requests.Session | None = None,\n) -> None:\n\"\"\"Initialize the pulse.eco API wrapper.\n        :param city_name: the city name\n        :param auth: a tuple of (email, password), defaults to None\n        :param base_url: the base URL of the API, defaults to\n            'https://{city_name}.pulse.eco/rest/{end_point}'\n        :param session: a requests session\n            , use this to customize the session and add retries, defaults to None\n        \"\"\"\nself.city_name = city_name\nif base_url is not None and PULSE_ECO_BASE_URL_FORMAT_ENV_KEY in os.environ:\nbase_url = os.environ[PULSE_ECO_BASE_URL_FORMAT_ENV_KEY]\nif session is not None:\nself._session = session\nelse:\nself._session = requests.Session()\nif auth is None:\nauth = get_auth_from_env(city_name=city_name)\nif auth is not None:\nself._session.auth = auth\nself._base_url = base_url\ndef _base_request(\nself, end_point: str, params: dict[str, Any] | None = None\n) -> Any:  # noqa: ANN401\n\"\"\"Make a request to the PulseEco API.\n        :param end_point: an end point of the API\n        :param params: get parameters, defaults to None\n        :return: the response json\n        \"\"\"\nif params is None:\nparams = {}\nurl = self._base_url.format(city_name=self.city_name, end_point=end_point)\nresponse = self._session.get(url, params=params)\nresponse.raise_for_status()\nreturn response.json()\ndef sensors(self) -> list[Sensor]:\n\"\"\"Get all sensors for a city.\n        :return: a list of sensors\n        \"\"\"\nreturn cast(\"list[Sensor]\", self._base_request(\"sensor\"))\ndef sensor(self, sensor_id: str) -> Sensor:\n\"\"\"Get a sensor by it's ID\n        :param sensor_id: the unique ID of the sensor\n        :return: a sensor\n        \"\"\"\nreturn cast(Sensor, self._base_request(f\"sensor/{sensor_id}\"))\ndef data_raw(\nself,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: str | None = None,\nsensor_id: str | None = None,\n) -> list[DataValueRaw]:\n\"\"\"Get raw data for a city.\n        :param from_: the start datetime of the data\n            as a datetime object or an isoformat string\n        :param to: the end datetime of the data\n            as a datetime object or an isoformat string\n        :param type: the data value type, defaults to None\n        :param sensor_id: the unique ID of the sensor, defaults to None\n        :return: a list of data values\n        \"\"\"\nif sensor_id is None and type is None:\nwarnings.warn(\n\"Warning! If you encounter an error, \"\n\"you should probably specify either sensor_id or type.\",\nstacklevel=2,\n)\ndata: list[DataValueRaw] = []\ndatetime_spans = split_datetime_span(from_, to, DATA_RAW_MAX_SPAN)\nfor from_temp, to_temp in datetime_spans:\nparams = {\n\"sensorId\": sensor_id,\n\"type\": type,\n\"from\": convert_datetime_to_str(from_temp),\n\"to\": convert_datetime_to_str(to_temp),\n}\nparams = {k: v for k, v in params.items() if v is not None}\ndata_value = cast(\n\"list[DataValueRaw]\",\nself._base_request(\"dataRaw\", params=params),\n)\ndata += data_value\nreturn data\ndef avg_data(\nself,\nperiod: str,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: str,\nsensor_id: str | None = None,\n) -> list[DataValueAvg]:\n\"\"\"Get average data for a city.\n        :param period: the period of the average data (day, week, month)\n        :param from_: the start datetime of the data\n            as a datetime object or an isoformat string\n        :param to: the end datetime of the data\n            as a datetime object or an isoformat string\n        :param type: the data value type\n        :param sensor_id: the unique ID of the sensor, defaults to None\n        :return: a list of average data values\n        \"\"\"\nif period not in {\"day\", \"week\", \"month\"}:\nwarnings.warn(\n\"Warning! Invalid value for period. \"\n\"Should be one of: day, week, month\",\nstacklevel=2,\n)\ndata: list[DataValueAvg] = []\ndatetime_spans = split_datetime_span(from_, to, AVG_DATA_MAX_SPAN)\nfor from_temp, to_temp in datetime_spans:\nparams = {\n\"sensorId\": sensor_id,\n\"type\": type,\n\"from\": convert_datetime_to_str(from_temp),\n\"to\": convert_datetime_to_str(to_temp),\n}\nparams = {k: v for k, v in params.items() if v is not None}\ndata_value = cast(\n\"list[DataValueAvg]\",\nself._base_request(f\"avgData/{period}\", params=params),\n)\ndata += data_value\nreturn data\ndef data24h(self) -> list[DataValueRaw]:\n\"\"\"Get 24h data for a city.\n        The data values are sorted ascending by their timestamp.\n        :return: a list of data values for the past 24 hours\n        \"\"\"\nreturn cast(\"list[DataValueRaw]\", self._base_request(\"data24h\"))\ndef current(self) -> list[DataValueRaw]:\n\"\"\"Get the last received valid data for each sensor in a city\n        Will not return sensor data older than 2 hours.\n        :return: a list of current data values\n        \"\"\"\nreturn cast(\"list[DataValueRaw]\", self._base_request(\"current\"))\ndef overall(\nself,\n) -> Overall:\n\"\"\"Get the current average data for all sensors per value for a city.\n        ## Example:\n        ```python\n        {\n            'cityName': 'skopje',\n            'values': {\n                'no2': '22',\n                'o3': '4',\n                'pm25': '53',\n                'pm10': '73',\n                'temperature': '7',\n                'humidity': '71',\n                'pressure': '992',\n                'noise_dba': '43'\n            }\n        }\n        ```\n        :return: the overall data for the city\n        \"\"\"\nreturn cast(Overall, self._base_request(\"overall\"))\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.__init__","title":"__init__(city_name, auth=None, base_url=PULSE_ECO_BASE_URL_FORMAT, session=None)","text":"

Initialize the pulse.eco API wrapper.

Parameters:

Name Type Description Default city_name str

the city name

required auth tuple[str, str] | None

a tuple of (email, password), defaults to None

None base_url str

the base URL of the API, defaults to 'https://{city_name}.pulse.eco/rest/{end_point}'

PULSE_ECO_BASE_URL_FORMAT session Session | None

a requests session , use this to customize the session and add retries, defaults to None

None Source code in pulseeco/api/pulse_eco_api.py
def __init__(\nself,\ncity_name: str,\nauth: tuple[str, str] | None = None,\nbase_url: str = PULSE_ECO_BASE_URL_FORMAT,\nsession: requests.Session | None = None,\n) -> None:\n\"\"\"Initialize the pulse.eco API wrapper.\n    :param city_name: the city name\n    :param auth: a tuple of (email, password), defaults to None\n    :param base_url: the base URL of the API, defaults to\n        'https://{city_name}.pulse.eco/rest/{end_point}'\n    :param session: a requests session\n        , use this to customize the session and add retries, defaults to None\n    \"\"\"\nself.city_name = city_name\nif base_url is not None and PULSE_ECO_BASE_URL_FORMAT_ENV_KEY in os.environ:\nbase_url = os.environ[PULSE_ECO_BASE_URL_FORMAT_ENV_KEY]\nif session is not None:\nself._session = session\nelse:\nself._session = requests.Session()\nif auth is None:\nauth = get_auth_from_env(city_name=city_name)\nif auth is not None:\nself._session.auth = auth\nself._base_url = base_url\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.avg_data","title":"avg_data(period, from_, to, type, sensor_id=None)","text":"

Get average data for a city.

Parameters:

Name Type Description Default period str

the period of the average data (day, week, month)

required from_ str | datetime

the start datetime of the data as a datetime object or an isoformat string

required to str | datetime

the end datetime of the data as a datetime object or an isoformat string

required type str

the data value type

required sensor_id str | None

the unique ID of the sensor, defaults to None

None

Returns:

Type Description list[DataValueAvg]

a list of average data values

Source code in pulseeco/api/pulse_eco_api.py
def avg_data(\nself,\nperiod: str,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: str,\nsensor_id: str | None = None,\n) -> list[DataValueAvg]:\n\"\"\"Get average data for a city.\n    :param period: the period of the average data (day, week, month)\n    :param from_: the start datetime of the data\n        as a datetime object or an isoformat string\n    :param to: the end datetime of the data\n        as a datetime object or an isoformat string\n    :param type: the data value type\n    :param sensor_id: the unique ID of the sensor, defaults to None\n    :return: a list of average data values\n    \"\"\"\nif period not in {\"day\", \"week\", \"month\"}:\nwarnings.warn(\n\"Warning! Invalid value for period. \"\n\"Should be one of: day, week, month\",\nstacklevel=2,\n)\ndata: list[DataValueAvg] = []\ndatetime_spans = split_datetime_span(from_, to, AVG_DATA_MAX_SPAN)\nfor from_temp, to_temp in datetime_spans:\nparams = {\n\"sensorId\": sensor_id,\n\"type\": type,\n\"from\": convert_datetime_to_str(from_temp),\n\"to\": convert_datetime_to_str(to_temp),\n}\nparams = {k: v for k, v in params.items() if v is not None}\ndata_value = cast(\n\"list[DataValueAvg]\",\nself._base_request(f\"avgData/{period}\", params=params),\n)\ndata += data_value\nreturn data\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.current","title":"current()","text":"

Get the last received valid data for each sensor in a city

Will not return sensor data older than 2 hours.

Returns:

Type Description list[DataValueRaw]

a list of current data values

Source code in pulseeco/api/pulse_eco_api.py
def current(self) -> list[DataValueRaw]:\n\"\"\"Get the last received valid data for each sensor in a city\n    Will not return sensor data older than 2 hours.\n    :return: a list of current data values\n    \"\"\"\nreturn cast(\"list[DataValueRaw]\", self._base_request(\"current\"))\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.data24h","title":"data24h()","text":"

Get 24h data for a city.

The data values are sorted ascending by their timestamp.

Returns:

Type Description list[DataValueRaw]

a list of data values for the past 24 hours

Source code in pulseeco/api/pulse_eco_api.py
def data24h(self) -> list[DataValueRaw]:\n\"\"\"Get 24h data for a city.\n    The data values are sorted ascending by their timestamp.\n    :return: a list of data values for the past 24 hours\n    \"\"\"\nreturn cast(\"list[DataValueRaw]\", self._base_request(\"data24h\"))\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.data_raw","title":"data_raw(from_, to, type=None, sensor_id=None)","text":"

Get raw data for a city.

Parameters:

Name Type Description Default from_ str | datetime

the start datetime of the data as a datetime object or an isoformat string

required to str | datetime

the end datetime of the data as a datetime object or an isoformat string

required type str | None

the data value type, defaults to None

None sensor_id str | None

the unique ID of the sensor, defaults to None

None

Returns:

Type Description list[DataValueRaw]

a list of data values

Source code in pulseeco/api/pulse_eco_api.py
def data_raw(\nself,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: str | None = None,\nsensor_id: str | None = None,\n) -> list[DataValueRaw]:\n\"\"\"Get raw data for a city.\n    :param from_: the start datetime of the data\n        as a datetime object or an isoformat string\n    :param to: the end datetime of the data\n        as a datetime object or an isoformat string\n    :param type: the data value type, defaults to None\n    :param sensor_id: the unique ID of the sensor, defaults to None\n    :return: a list of data values\n    \"\"\"\nif sensor_id is None and type is None:\nwarnings.warn(\n\"Warning! If you encounter an error, \"\n\"you should probably specify either sensor_id or type.\",\nstacklevel=2,\n)\ndata: list[DataValueRaw] = []\ndatetime_spans = split_datetime_span(from_, to, DATA_RAW_MAX_SPAN)\nfor from_temp, to_temp in datetime_spans:\nparams = {\n\"sensorId\": sensor_id,\n\"type\": type,\n\"from\": convert_datetime_to_str(from_temp),\n\"to\": convert_datetime_to_str(to_temp),\n}\nparams = {k: v for k, v in params.items() if v is not None}\ndata_value = cast(\n\"list[DataValueRaw]\",\nself._base_request(\"dataRaw\", params=params),\n)\ndata += data_value\nreturn data\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.overall","title":"overall()","text":"

Get the current average data for all sensors per value for a city.

"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.overall--example","title":"Example:","text":"
{\n'cityName': 'skopje',\n'values': {\n'no2': '22',\n'o3': '4',\n'pm25': '53',\n'pm10': '73',\n'temperature': '7',\n'humidity': '71',\n'pressure': '992',\n'noise_dba': '43'\n}\n}\n

Returns:

Type Description Overall

the overall data for the city

Source code in pulseeco/api/pulse_eco_api.py
def overall(\nself,\n) -> Overall:\n\"\"\"Get the current average data for all sensors per value for a city.\n    ## Example:\n    ```python\n    {\n        'cityName': 'skopje',\n        'values': {\n            'no2': '22',\n            'o3': '4',\n            'pm25': '53',\n            'pm10': '73',\n            'temperature': '7',\n            'humidity': '71',\n            'pressure': '992',\n            'noise_dba': '43'\n        }\n    }\n    ```\n    :return: the overall data for the city\n    \"\"\"\nreturn cast(Overall, self._base_request(\"overall\"))\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.sensor","title":"sensor(sensor_id)","text":"

Get a sensor by it's ID

Parameters:

Name Type Description Default sensor_id str

the unique ID of the sensor

required

Returns:

Type Description Sensor

a sensor

Source code in pulseeco/api/pulse_eco_api.py
def sensor(self, sensor_id: str) -> Sensor:\n\"\"\"Get a sensor by it's ID\n    :param sensor_id: the unique ID of the sensor\n    :return: a sensor\n    \"\"\"\nreturn cast(Sensor, self._base_request(f\"sensor/{sensor_id}\"))\n
"},{"location":"PulseEcoAPI/#pulseeco.api.PulseEcoAPI.sensors","title":"sensors()","text":"

Get all sensors for a city.

Returns:

Type Description list[Sensor]

a list of sensors

Source code in pulseeco/api/pulse_eco_api.py
def sensors(self) -> list[Sensor]:\n\"\"\"Get all sensors for a city.\n    :return: a list of sensors\n    \"\"\"\nreturn cast(\"list[Sensor]\", self._base_request(\"sensor\"))\n
"},{"location":"PulseEcoClient/","title":"PulseEcoClient","text":""},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient","title":"PulseEcoClient","text":"

High level pulse.eco client.

Source code in pulseeco/client.py
class PulseEcoClient:\n\"\"\"High level pulse.eco client.\"\"\"\ndef __init__(\nself,\ncity_name: str,\nauth: tuple[str, str] | None = None,\nbase_url: str = PULSE_ECO_BASE_URL_FORMAT,\nsession: requests.Session | None = None,\npulse_eco_api: PulseEcoAPIBase | None = None,\n) -> None:\n\"\"\"Initialize the pulse.eco client.\n        :param city_name: the city name\n        :param auth: a tuple of (email, password), defaults to None\n        :param base_url: the base URL of the API, defaults to\n            'https://{city_name}.pulse.eco/rest/{end_point}'\n        :param session: a requests session\n            use this to customize the session and add retries, defaults to None,\n        :param pulse_eco_api: a pulse.eco API wrapper, defaults to None,\n            if set, the other parameters are ignored\n        \"\"\"\nself._pulse_eco_api: PulseEcoAPIBase\nif pulse_eco_api is None:\nself._pulse_eco_api = PulseEcoAPI(\ncity_name=city_name, auth=auth, base_url=base_url, session=session\n)\nelse:\nself._pulse_eco_api = pulse_eco_api\ndef sensors(self) -> list[Sensor]:\n\"\"\"Get all sensors for a city.\n        :return: a list of sensors\n        \"\"\"\nreturn [\nSensor.model_validate(sensor) for sensor in self._pulse_eco_api.sensors()\n]\ndef sensor(self, sensor_id: str) -> Sensor:\n\"\"\"Get a sensor by it's ID.\n        :param sensor_id: the unique ID of the sensor\n        :return: a sensor\n        \"\"\"\nreturn Sensor.model_validate(self._pulse_eco_api.sensor(sensor_id=sensor_id))\ndef data_raw(\nself,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: DataValueType | None = None,\nsensor_id: str | None = None,\n) -> list[DataValue]:\n\"\"\"Get raw data for a city.\n        :param from_: the start datetime of the data\n            as a datetime object or an isoformat string\n        :param to: the end datetime of the data\n            as a datetime object or an isoformat string\n        :param type: the data value type, defaults to None\n        :param sensor_id: the unique ID of the sensor, defaults to None\n        :return: a list of data values\n        \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.data_raw(\nfrom_=from_,\nto=to,\ntype=type,\nsensor_id=sensor_id,\n)\n]\ndef avg_data(\nself,\nperiod: AveragePeriod,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: DataValueType,\nsensor_id: str | None = None,\n) -> list[DataValue]:\n\"\"\"Get average data for a city.\n        :param period: the period of the average data\n        :param from_: the start datetime of the data\n            as a datetime object or an isoformat string\n        :param to: the end datetime of the data\n            as a datetime object or an isoformat string\n        :param type: the data value type\n        :param sensor_id: the unique ID of the sensor, defaults to None\n        :return: a list of average data values\n        \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.avg_data(\nperiod=period,\nfrom_=from_,\nto=to,\ntype=type,\nsensor_id=sensor_id,\n)\n]\ndef data24h(self) -> list[DataValue]:\n\"\"\"Get 24h data for a city.\n        The data values are sorted ascending by their timestamp.\n        :return: a list of data values for the past 24 hours\n        \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.data24h()\n]\ndef current(self) -> list[DataValue]:\n\"\"\"Get the last received valid data for each sensor in a city.\n        Will not return sensor data older than 2 hours.\n        :return: a list of current data values\n        \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.current()\n]\ndef overall(self) -> Overall:\n\"\"\"Get the current average data for all sensors per value for a city.\n        :return: the overall data for the city\n        \"\"\"\nreturn Overall.model_validate(self._pulse_eco_api.overall())\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.__init__","title":"__init__(city_name, auth=None, base_url=PULSE_ECO_BASE_URL_FORMAT, session=None, pulse_eco_api=None)","text":"

Initialize the pulse.eco client.

Parameters:

Name Type Description Default city_name str

the city name

required auth tuple[str, str] | None

a tuple of (email, password), defaults to None

None base_url str

the base URL of the API, defaults to 'https://{city_name}.pulse.eco/rest/{end_point}'

PULSE_ECO_BASE_URL_FORMAT session Session | None

a requests session use this to customize the session and add retries, defaults to None,

None pulse_eco_api PulseEcoAPIBase | None

a pulse.eco API wrapper, defaults to None, if set, the other parameters are ignored

None Source code in pulseeco/client.py
def __init__(\nself,\ncity_name: str,\nauth: tuple[str, str] | None = None,\nbase_url: str = PULSE_ECO_BASE_URL_FORMAT,\nsession: requests.Session | None = None,\npulse_eco_api: PulseEcoAPIBase | None = None,\n) -> None:\n\"\"\"Initialize the pulse.eco client.\n    :param city_name: the city name\n    :param auth: a tuple of (email, password), defaults to None\n    :param base_url: the base URL of the API, defaults to\n        'https://{city_name}.pulse.eco/rest/{end_point}'\n    :param session: a requests session\n        use this to customize the session and add retries, defaults to None,\n    :param pulse_eco_api: a pulse.eco API wrapper, defaults to None,\n        if set, the other parameters are ignored\n    \"\"\"\nself._pulse_eco_api: PulseEcoAPIBase\nif pulse_eco_api is None:\nself._pulse_eco_api = PulseEcoAPI(\ncity_name=city_name, auth=auth, base_url=base_url, session=session\n)\nelse:\nself._pulse_eco_api = pulse_eco_api\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.avg_data","title":"avg_data(period, from_, to, type, sensor_id=None)","text":"

Get average data for a city.

Parameters:

Name Type Description Default period AveragePeriod

the period of the average data

required from_ str | datetime

the start datetime of the data as a datetime object or an isoformat string

required to str | datetime

the end datetime of the data as a datetime object or an isoformat string

required type DataValueType

the data value type

required sensor_id str | None

the unique ID of the sensor, defaults to None

None

Returns:

Type Description list[DataValue]

a list of average data values

Source code in pulseeco/client.py
def avg_data(\nself,\nperiod: AveragePeriod,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: DataValueType,\nsensor_id: str | None = None,\n) -> list[DataValue]:\n\"\"\"Get average data for a city.\n    :param period: the period of the average data\n    :param from_: the start datetime of the data\n        as a datetime object or an isoformat string\n    :param to: the end datetime of the data\n        as a datetime object or an isoformat string\n    :param type: the data value type\n    :param sensor_id: the unique ID of the sensor, defaults to None\n    :return: a list of average data values\n    \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.avg_data(\nperiod=period,\nfrom_=from_,\nto=to,\ntype=type,\nsensor_id=sensor_id,\n)\n]\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.current","title":"current()","text":"

Get the last received valid data for each sensor in a city.

Will not return sensor data older than 2 hours.

Returns:

Type Description list[DataValue]

a list of current data values

Source code in pulseeco/client.py
def current(self) -> list[DataValue]:\n\"\"\"Get the last received valid data for each sensor in a city.\n    Will not return sensor data older than 2 hours.\n    :return: a list of current data values\n    \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.current()\n]\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.data24h","title":"data24h()","text":"

Get 24h data for a city.

The data values are sorted ascending by their timestamp.

Returns:

Type Description list[DataValue]

a list of data values for the past 24 hours

Source code in pulseeco/client.py
def data24h(self) -> list[DataValue]:\n\"\"\"Get 24h data for a city.\n    The data values are sorted ascending by their timestamp.\n    :return: a list of data values for the past 24 hours\n    \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.data24h()\n]\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.data_raw","title":"data_raw(from_, to, type=None, sensor_id=None)","text":"

Get raw data for a city.

Parameters:

Name Type Description Default from_ str | datetime

the start datetime of the data as a datetime object or an isoformat string

required to str | datetime

the end datetime of the data as a datetime object or an isoformat string

required type DataValueType | None

the data value type, defaults to None

None sensor_id str | None

the unique ID of the sensor, defaults to None

None

Returns:

Type Description list[DataValue]

a list of data values

Source code in pulseeco/client.py
def data_raw(\nself,\nfrom_: str | datetime.datetime,\nto: str | datetime.datetime,\ntype: DataValueType | None = None,\nsensor_id: str | None = None,\n) -> list[DataValue]:\n\"\"\"Get raw data for a city.\n    :param from_: the start datetime of the data\n        as a datetime object or an isoformat string\n    :param to: the end datetime of the data\n        as a datetime object or an isoformat string\n    :param type: the data value type, defaults to None\n    :param sensor_id: the unique ID of the sensor, defaults to None\n    :return: a list of data values\n    \"\"\"\nreturn [\nDataValue.model_validate(data_value)\nfor data_value in self._pulse_eco_api.data_raw(\nfrom_=from_,\nto=to,\ntype=type,\nsensor_id=sensor_id,\n)\n]\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.overall","title":"overall()","text":"

Get the current average data for all sensors per value for a city.

Returns:

Type Description Overall

the overall data for the city

Source code in pulseeco/client.py
def overall(self) -> Overall:\n\"\"\"Get the current average data for all sensors per value for a city.\n    :return: the overall data for the city\n    \"\"\"\nreturn Overall.model_validate(self._pulse_eco_api.overall())\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.sensor","title":"sensor(sensor_id)","text":"

Get a sensor by it's ID.

Parameters:

Name Type Description Default sensor_id str

the unique ID of the sensor

required

Returns:

Type Description Sensor

a sensor

Source code in pulseeco/client.py
def sensor(self, sensor_id: str) -> Sensor:\n\"\"\"Get a sensor by it's ID.\n    :param sensor_id: the unique ID of the sensor\n    :return: a sensor\n    \"\"\"\nreturn Sensor.model_validate(self._pulse_eco_api.sensor(sensor_id=sensor_id))\n
"},{"location":"PulseEcoClient/#pulseeco.client.PulseEcoClient.sensors","title":"sensors()","text":"

Get all sensors for a city.

Returns:

Type Description list[Sensor]

a list of sensors

Source code in pulseeco/client.py
def sensors(self) -> list[Sensor]:\n\"\"\"Get all sensors for a city.\n    :return: a list of sensors\n    \"\"\"\nreturn [\nSensor.model_validate(sensor) for sensor in self._pulse_eco_api.sensors()\n]\n
"},{"location":"environment-variables/","title":"Environment variables","text":""},{"location":"environment-variables/#base-url-format","title":"Base URL format","text":"

Environment variable: PULSE_ECO_BASE_URL_FORMAT

The default base URL format is https://{city_name}.pulse.eco/rest/{end_point}.

"},{"location":"environment-variables/#authentication","title":"Authentication","text":"

Authentication is not required for fetching data. But if provided, it has to be valid for the city.

Credentials can also be provided as environment variables. To provide credentials for a city, use the following format:

PULSE_ECO_{city_name}_USERNAME\nPULSE_ECO_{city_name}_PASSWORD\n

Example environmtent variables in priority order:

PULSE_ECO_SKOPJE_USERNAME\nPULSE_ECO_SKOPJE_PASSWORD\n\nPULSE_ECO_skopje_USERNAME\nPULSE_ECO_skopje_PASSWORD\n\nPULSE_ECO_USERNAME\nPULSE_ECO_PASSWORD\n

Only use the generic PULSE_ECO_USERNAME and PULSE_ECO_PASSWORD environment variables if your application requests data from a single city.

"},{"location":"example-usage/","title":"Example usage","text":""},{"location":"example-usage/#initialize-client","title":"Initialize client","text":"

Authentication is not required for fetching data. But if provided, it has to be valid. Authentication is per city.

from pulseeco import PulseEcoClient\npulse_eco = PulseEcoClient(city_name=\"skopje\", auth=(\"user\", \"pass\"))\n
"},{"location":"example-usage/#get-all-sensors","title":"Get all sensors","text":"
>>> pulse_eco.sensors()\n[\n  Sensor(\n    sensor_id='sensor_dev_60237_141',\n    position='42.03900255426,21.40771061182',\n    comments='Imported Sensor.community #60237',\n    type='20004',\n    description='Sensor.community 60237',\n    status='NOT_CLAIMED'\n  ),\n  Sensor(\n    sensor_id='sensor_dev_10699_244',\n    position='41.986,21.452',\n    comments='Imported Sensor.community #10699',\n    type='20004',\n    description='Sensor.community 10699',\n    status='NOT_CLAIMED_UNCONFIRMED'\n  ),\n  Sensor(\n    sensor_id='66710fdc-cdfc-4bbe-93a8-7e796fb8a88d',\n    position='41.995238146587674,21.402708292007443',\n    comments='V1 WiFi sensor in Kozle',\n    type='1',\n    description='Kozle',\n    status='ACTIVE'\n  ),\n  ...\n]\n
"},{"location":"example-usage/#get-a-sensor-by-id","title":"Get a sensor by id","text":"
>>> pulse_eco.sensor(sensor_id=\"1000\")\nSensor(\n  sensor_id='1000',\n  position='41.99249998,21.4236110',\n  comments='MOEPP sensor at Centar',\n  type='0',\n  description='MOEPP Centar',\n  status='ACTIVE'\n)\n
"},{"location":"example-usage/#get-raw-data","title":"Get raw data","text":"

from_ and to can be either datetime.datetime objects or str in ISO 8601 format.

>>> import datetime\n>>> from pulseeco import DataValueType\n>>> pulse_eco.data_raw(\n...   from_=datetime.datetime(year=2017, month=3, day=15, hour=2),\n...   to=datetime.datetime(year=2017, month=4, day=19, hour=12),\n...   type=DataValueType.PM10,\n...   sensor_id=\"1001\",\n... )\n[\n  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 3, 15, 3, 0, 8, tzinfo=TzInfo(+01:00)), type='pm10', position='41.9783,21.47', value=28, year=None),\n  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 3, 15, 4, 0, 8, tzinfo=TzInfo(+01:00)), type='pm10', position='41.9783,21.47', value=55, year=None),\n  ...\n  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 4, 19, 12, 0, 9, tzinfo=TzInfo(+02:00)), type='pm10', position='41.9783,21.47', value=6, year=None),\n  DataValue(sensor_id='1001', stamp=datetime.datetime(2017, 4, 19, 13, 0, 9, tzinfo=TzInfo(+02:00)), type='pm10', position='41.9783,21.47', value=31, year=None)\n]\n
"},{"location":"example-usage/#get-average-data","title":"Get average data","text":"

sensor_id \"-1\" is a magic value that gives average values for the whole city.

>>> import datetime\n>>> from pulseeco import AveragePeriod, DataValueType\n>>> pulse_eco.avg_data(\n...   period=AveragePeriod.MONTH,\n...   from_=datetime.datetime(year=2019, month=3, day=1, hour=12),\n...   to=datetime.datetime(year=2020, month=5, day=1, hour=12),\n...   type=DataValueType.PM10,\n...   sensor_id=\"-1\",\n... )\n[\n  DataValue(sensor_id='-1', stamp=datetime.datetime(2019, 3, 1, 13, 0, tzinfo=TzInfo(+01:00)), type='pm10', position='', value=29, year=None),\n  DataValue(sensor_id='-1', stamp=datetime.datetime(2019, 4, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=19, year=None),\n  ...\n  DataValue(sensor_id='-1', stamp=datetime.datetime(2020, 4, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=17, year=None),\n  DataValue(sensor_id='-1', stamp=datetime.datetime(2020, 5, 1, 14, 0, tzinfo=TzInfo(+02:00)), type='pm10', position='', value=12, year=None)\n]\n
"},{"location":"example-usage/#get-24h-data","title":"Get 24h data","text":"
>>> pulse_eco.data24h()\n[ ... ]\n
"},{"location":"example-usage/#get-current-data","title":"Get current data","text":"

Get the last received valid data for each sensor in a city.

>>> pulse_eco.current()\n[ ... ]\n
"},{"location":"example-usage/#get-overall-data","title":"Get overall data","text":"

Get the current average data for all sensors per value for a city.

>>> pulse_eco.overall()\nOverall(\n  city_name='skopje',\n  values=OverallValues(\n    no2=6,\n    o3=10,\n    so2=None,\n    co=None,\n    pm25=56,\n    pm10=95,\n    temperature=6,\n    humidity=73,\n    pressure=995,\n    noise=None,\n    noise_dba=42,\n    gas_resistance=None\n  )\n)\n
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..9edd751 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,28 @@ + + + + None + 2024-03-12 + daily + + + None + 2024-03-12 + daily + + + None + 2024-03-12 + daily + + + None + 2024-03-12 + daily + + + None + 2024-03-12 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 0000000..dc732e1 Binary files /dev/null and b/sitemap.xml.gz differ