e-data 1.2.16__tar.gz → 1.2.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- e-data-1.2.18/PKG-INFO +128 -0
- e-data-1.2.18/e_data.egg-info/PKG-INFO +128 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/connectors/datadis.py +73 -55
- {e-data-1.2.16 → e-data-1.2.18}/edata/helpers.py +12 -10
- {e-data-1.2.16 → e-data-1.2.18}/setup.py +1 -1
- e-data-1.2.16/PKG-INFO +0 -128
- e-data-1.2.16/e_data.egg-info/PKG-INFO +0 -128
- {e-data-1.2.16 → e-data-1.2.18}/LICENSE +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/MANIFEST.in +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/README.md +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/e_data.egg-info/SOURCES.txt +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/e_data.egg-info/dependency_links.txt +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/e_data.egg-info/requires.txt +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/e_data.egg-info/top_level.txt +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/__init__.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/connectors/__init__.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/connectors/redata.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/const.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/definitions.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/__init__.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/base.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/billing.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/consumption.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/maximeter.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/processors/utils.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/edata/storage.py +0 -0
- {e-data-1.2.16 → e-data-1.2.18}/setup.cfg +0 -0
e-data-1.2.18/PKG-INFO
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: e-data
|
|
3
|
+
Version: 1.2.18
|
|
4
|
+
Summary: Python library for managing spanish energy data from various web providers
|
|
5
|
+
Home-page: https://github.com/uvejota/python-edata
|
|
6
|
+
Author: VMG
|
|
7
|
+
Author-email: vmayorg@outlook.es
|
|
8
|
+
License: GPLv3
|
|
9
|
+
Description:
|
|
10
|
+
[](https://pepy.tech/project/e-data)
|
|
11
|
+
[](https://pepy.tech/project/e-data)
|
|
12
|
+
[](https://pepy.tech/project/e-data)
|
|
13
|
+
|
|
14
|
+
# python-edata
|
|
15
|
+
|
|
16
|
+
Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
|
|
17
|
+
|
|
18
|
+
_**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
|
|
19
|
+
|
|
20
|
+
## Instalación
|
|
21
|
+
|
|
22
|
+
Puedes instalar la última versión estable mediante:
|
|
23
|
+
|
|
24
|
+
``` bash
|
|
25
|
+
pip install e-data
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
|
|
29
|
+
|
|
30
|
+
``` bash
|
|
31
|
+
pip install -r requirements.txt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Estructura
|
|
35
|
+
|
|
36
|
+
El paquete consta de tres módulos diferenciados:
|
|
37
|
+
|
|
38
|
+
* **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
|
|
39
|
+
* **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
|
|
40
|
+
* **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
|
|
41
|
+
|
|
42
|
+
Estos módulos corresponden a la siguiente estructura del paquete:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
edata/
|
|
46
|
+
· __init__.py
|
|
47
|
+
· connectors/
|
|
48
|
+
· __init__.py
|
|
49
|
+
· datadis.py
|
|
50
|
+
· redata.py
|
|
51
|
+
· processors/
|
|
52
|
+
· __init__.py
|
|
53
|
+
· base.py
|
|
54
|
+
· billing.py
|
|
55
|
+
· consumption.py
|
|
56
|
+
· maximeter.py
|
|
57
|
+
· utils.py
|
|
58
|
+
· helpers.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Ejemplo de uso
|
|
62
|
+
|
|
63
|
+
Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
|
|
64
|
+
* No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
|
|
65
|
+
* El username suele ser el NIF del titular
|
|
66
|
+
* Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
|
|
67
|
+
* La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
|
|
68
|
+
|
|
69
|
+
``` python
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
import json
|
|
72
|
+
|
|
73
|
+
# importamos definiciones de datos que nos interesen
|
|
74
|
+
from edata.definitions import PricingRules
|
|
75
|
+
# importamos el ayudante
|
|
76
|
+
from edata.helpers import EdataHelper
|
|
77
|
+
# importamos el procesador de utilidades
|
|
78
|
+
from edata.processors import utils
|
|
79
|
+
|
|
80
|
+
# Preparar reglas de tarificación (si se quiere)
|
|
81
|
+
PRICING_RULES_PVPC = PricingRules(
|
|
82
|
+
p1_kw_year_eur=30.67266,
|
|
83
|
+
p2_kw_year_eur=1.4243591,
|
|
84
|
+
meter_month_eur=0.81,
|
|
85
|
+
market_kw_year_eur=3.113,
|
|
86
|
+
electricity_tax=1.0511300560,
|
|
87
|
+
iva_tax=1.05,
|
|
88
|
+
# podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
|
|
89
|
+
p1_kwh_eur=None,
|
|
90
|
+
p2_kwh_eur=None,
|
|
91
|
+
p3_kwh_eur=None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Instanciar el helper
|
|
95
|
+
# 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
|
|
96
|
+
# 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
|
|
97
|
+
edata = EdataHelper(
|
|
98
|
+
"datadis_user",
|
|
99
|
+
"datadis_password",
|
|
100
|
+
"cups",
|
|
101
|
+
datadis_authorized_nif=None,
|
|
102
|
+
pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
|
|
103
|
+
data=None, # aquí podríamos cargar datos anteriores
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Solicitar actualización de todo el histórico (se almacena en edata.data)
|
|
107
|
+
edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
|
|
108
|
+
|
|
109
|
+
# volcamos todo lo obtenido a un fichero
|
|
110
|
+
with open("backup.json", "w") as file:
|
|
111
|
+
json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
|
|
112
|
+
|
|
113
|
+
# Imprimir atributos
|
|
114
|
+
print(edata.attributes)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Platform: UNKNOWN
|
|
118
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
119
|
+
Classifier: Programming Language :: Python
|
|
120
|
+
Classifier: Programming Language :: Python :: 3
|
|
121
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
122
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
123
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
124
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
125
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
126
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
127
|
+
Requires-Python: >=3.6.0
|
|
128
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: e-data
|
|
3
|
+
Version: 1.2.18
|
|
4
|
+
Summary: Python library for managing spanish energy data from various web providers
|
|
5
|
+
Home-page: https://github.com/uvejota/python-edata
|
|
6
|
+
Author: VMG
|
|
7
|
+
Author-email: vmayorg@outlook.es
|
|
8
|
+
License: GPLv3
|
|
9
|
+
Description:
|
|
10
|
+
[](https://pepy.tech/project/e-data)
|
|
11
|
+
[](https://pepy.tech/project/e-data)
|
|
12
|
+
[](https://pepy.tech/project/e-data)
|
|
13
|
+
|
|
14
|
+
# python-edata
|
|
15
|
+
|
|
16
|
+
Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
|
|
17
|
+
|
|
18
|
+
_**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
|
|
19
|
+
|
|
20
|
+
## Instalación
|
|
21
|
+
|
|
22
|
+
Puedes instalar la última versión estable mediante:
|
|
23
|
+
|
|
24
|
+
``` bash
|
|
25
|
+
pip install e-data
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
|
|
29
|
+
|
|
30
|
+
``` bash
|
|
31
|
+
pip install -r requirements.txt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Estructura
|
|
35
|
+
|
|
36
|
+
El paquete consta de tres módulos diferenciados:
|
|
37
|
+
|
|
38
|
+
* **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
|
|
39
|
+
* **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
|
|
40
|
+
* **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
|
|
41
|
+
|
|
42
|
+
Estos módulos corresponden a la siguiente estructura del paquete:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
edata/
|
|
46
|
+
· __init__.py
|
|
47
|
+
· connectors/
|
|
48
|
+
· __init__.py
|
|
49
|
+
· datadis.py
|
|
50
|
+
· redata.py
|
|
51
|
+
· processors/
|
|
52
|
+
· __init__.py
|
|
53
|
+
· base.py
|
|
54
|
+
· billing.py
|
|
55
|
+
· consumption.py
|
|
56
|
+
· maximeter.py
|
|
57
|
+
· utils.py
|
|
58
|
+
· helpers.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Ejemplo de uso
|
|
62
|
+
|
|
63
|
+
Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
|
|
64
|
+
* No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
|
|
65
|
+
* El username suele ser el NIF del titular
|
|
66
|
+
* Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
|
|
67
|
+
* La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
|
|
68
|
+
|
|
69
|
+
``` python
|
|
70
|
+
from datetime import datetime
|
|
71
|
+
import json
|
|
72
|
+
|
|
73
|
+
# importamos definiciones de datos que nos interesen
|
|
74
|
+
from edata.definitions import PricingRules
|
|
75
|
+
# importamos el ayudante
|
|
76
|
+
from edata.helpers import EdataHelper
|
|
77
|
+
# importamos el procesador de utilidades
|
|
78
|
+
from edata.processors import utils
|
|
79
|
+
|
|
80
|
+
# Preparar reglas de tarificación (si se quiere)
|
|
81
|
+
PRICING_RULES_PVPC = PricingRules(
|
|
82
|
+
p1_kw_year_eur=30.67266,
|
|
83
|
+
p2_kw_year_eur=1.4243591,
|
|
84
|
+
meter_month_eur=0.81,
|
|
85
|
+
market_kw_year_eur=3.113,
|
|
86
|
+
electricity_tax=1.0511300560,
|
|
87
|
+
iva_tax=1.05,
|
|
88
|
+
# podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
|
|
89
|
+
p1_kwh_eur=None,
|
|
90
|
+
p2_kwh_eur=None,
|
|
91
|
+
p3_kwh_eur=None,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Instanciar el helper
|
|
95
|
+
# 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
|
|
96
|
+
# 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
|
|
97
|
+
edata = EdataHelper(
|
|
98
|
+
"datadis_user",
|
|
99
|
+
"datadis_password",
|
|
100
|
+
"cups",
|
|
101
|
+
datadis_authorized_nif=None,
|
|
102
|
+
pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
|
|
103
|
+
data=None, # aquí podríamos cargar datos anteriores
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Solicitar actualización de todo el histórico (se almacena en edata.data)
|
|
107
|
+
edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
|
|
108
|
+
|
|
109
|
+
# volcamos todo lo obtenido a un fichero
|
|
110
|
+
with open("backup.json", "w") as file:
|
|
111
|
+
json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
|
|
112
|
+
|
|
113
|
+
# Imprimir atributos
|
|
114
|
+
print(edata.attributes)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Platform: UNKNOWN
|
|
118
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
119
|
+
Classifier: Programming Language :: Python
|
|
120
|
+
Classifier: Programming Language :: Python :: 3
|
|
121
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
122
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
123
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
124
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
125
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
126
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
127
|
+
Requires-Python: >=3.6.0
|
|
128
|
+
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,6 @@ To fetch data from datadis.es private API.
|
|
|
4
4
|
There a few issues that are workarounded:
|
|
5
5
|
- You have to wait 24h between two identical requests.
|
|
6
6
|
- Datadis server does not like ranges greater than 1 month.
|
|
7
|
-
- TODO: gzip header checksum errors under weird circumstances
|
|
8
7
|
"""
|
|
9
8
|
|
|
10
9
|
import contextlib
|
|
@@ -17,6 +16,8 @@ from dateutil.relativedelta import relativedelta
|
|
|
17
16
|
|
|
18
17
|
import requests
|
|
19
18
|
|
|
19
|
+
from filelock import FileLock
|
|
20
|
+
|
|
20
21
|
from ..definitions import ConsumptionData, ContractData, MaxPowerData, SupplyData
|
|
21
22
|
from ..processors import utils
|
|
22
23
|
|
|
@@ -104,50 +105,59 @@ class DatadisConnector:
|
|
|
104
105
|
self._recent_queries_file = DEFAULT_RECENT_QUERIES_FILE
|
|
105
106
|
self._recent_queries_cache_file = DEFAULT_RECENT_QUERIES_CACHE
|
|
106
107
|
self._warned_queries = []
|
|
108
|
+
self._load_recent_cache()
|
|
109
|
+
|
|
110
|
+
def _load_recent_cache(self) -> None:
|
|
111
|
+
"""Load caches."""
|
|
107
112
|
|
|
108
|
-
# load caches (to avoid query spam if the program restarts)
|
|
109
113
|
with contextlib.suppress(FileNotFoundError):
|
|
110
114
|
with open(self._recent_queries_file, encoding="utf8") as dst_file:
|
|
111
|
-
|
|
112
|
-
for query in
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
recent_queries = json.load(dst_file)
|
|
116
|
+
for query in recent_queries:
|
|
117
|
+
recent_queries[query] = datetime.fromisoformat(
|
|
118
|
+
recent_queries[query]
|
|
115
119
|
)
|
|
120
|
+
self._recent_queries.update(recent_queries)
|
|
121
|
+
|
|
116
122
|
with open(self._recent_queries_cache_file, encoding="utf8") as dst_file:
|
|
117
|
-
self._recent_cache
|
|
123
|
+
self._recent_cache.update(json.load(dst_file))
|
|
118
124
|
|
|
119
125
|
def _update_recent_queries(self, query: str, data: dict | None = None) -> None:
|
|
120
126
|
"""Cache a successful query to avoid exceeding query limits."""
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
self.
|
|
128
|
-
|
|
129
|
-
# purge old cache
|
|
130
|
-
to_delete = []
|
|
131
|
-
for _query in self._recent_queries:
|
|
132
|
-
if (datetime.now() - self._recent_queries[_query]) > QUERY_LIMIT:
|
|
133
|
-
to_delete.append(_query)
|
|
134
|
-
|
|
135
|
-
for key in to_delete:
|
|
136
|
-
with contextlib.suppress(KeyError):
|
|
137
|
-
self._recent_queries.pop(key, None)
|
|
138
|
-
self._recent_cache.pop(key, None)
|
|
139
|
-
|
|
140
|
-
# dump current cache to disk
|
|
141
|
-
try:
|
|
142
|
-
with open(self._recent_queries_file, "w", encoding="utf8") as dst_file:
|
|
143
|
-
json.dump(utils.serialize_dict(self._recent_queries), dst_file)
|
|
128
|
+
with FileLock(self._recent_queries_file + ".lock"):
|
|
129
|
+
self._load_recent_cache()
|
|
130
|
+
# identify the query by a md5 hash
|
|
131
|
+
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
132
|
+
# prepare key (hash) and values (timestamp and response)
|
|
133
|
+
self._recent_queries[hash_query] = datetime.now()
|
|
144
134
|
if data is not None:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
135
|
+
self._recent_cache[hash_query] = data
|
|
136
|
+
|
|
137
|
+
# purge old cache
|
|
138
|
+
to_delete = [
|
|
139
|
+
x
|
|
140
|
+
for x in self._recent_queries
|
|
141
|
+
if (datetime.now() - self._recent_queries[x]) > QUERY_LIMIT
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
for key in to_delete:
|
|
145
|
+
with contextlib.suppress(KeyError):
|
|
146
|
+
self._recent_queries.pop(key, None)
|
|
147
|
+
self._recent_cache.pop(key, None)
|
|
148
|
+
|
|
149
|
+
# dump current cache to disk
|
|
150
|
+
try:
|
|
151
|
+
with open(self._recent_queries_file, "w", encoding="utf8") as dst_file:
|
|
152
|
+
json.dump(utils.serialize_dict(self._recent_queries), dst_file)
|
|
153
|
+
if data is not None:
|
|
154
|
+
with open(
|
|
155
|
+
self._recent_queries_cache_file, "w", encoding="utf8"
|
|
156
|
+
) as dst_file:
|
|
157
|
+
json.dump(self._recent_cache, dst_file)
|
|
158
|
+
_LOGGER.info("Local cache updated")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
_LOGGER.warning("Unknown error while updating cache: %s", e)
|
|
151
161
|
|
|
152
162
|
def _is_recent_query(self, query: str) -> bool:
|
|
153
163
|
"""Check if a query has been done recently to avoid exceeding query limits."""
|
|
@@ -160,7 +170,7 @@ class DatadisConnector:
|
|
|
160
170
|
def _get_cache_for_query(self, query: str) -> dict:
|
|
161
171
|
"""Return cached response for a query."""
|
|
162
172
|
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
163
|
-
|
|
173
|
+
_LOGGER.info("Serving '%s' from cache", query)
|
|
164
174
|
return self._recent_cache.get(hash_query, None)
|
|
165
175
|
|
|
166
176
|
def _get_token(self):
|
|
@@ -302,7 +312,7 @@ class DatadisConnector:
|
|
|
302
312
|
|
|
303
313
|
# Request the resource
|
|
304
314
|
response = self._get(
|
|
305
|
-
URL_GET_SUPPLIES, request_data=data, ignore_recent_queries=
|
|
315
|
+
URL_GET_SUPPLIES, request_data=data, ignore_recent_queries=True
|
|
306
316
|
)
|
|
307
317
|
|
|
308
318
|
# Response is a list of serialized supplies.
|
|
@@ -318,23 +328,27 @@ class DatadisConnector:
|
|
|
318
328
|
SupplyData(
|
|
319
329
|
cups=i["cups"], # the supply identifier
|
|
320
330
|
date_start=datetime.strptime(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
(
|
|
332
|
+
i["validDateFrom"]
|
|
333
|
+
if i["validDateFrom"] != ""
|
|
334
|
+
else "1970/01/01"
|
|
335
|
+
),
|
|
324
336
|
"%Y/%m/%d",
|
|
325
337
|
), # start date of the supply. 1970/01/01 if unset.
|
|
326
338
|
date_end=datetime.strptime(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
339
|
+
(
|
|
340
|
+
i["validDateTo"]
|
|
341
|
+
if i["validDateTo"] != ""
|
|
342
|
+
else tomorrow_str
|
|
343
|
+
),
|
|
330
344
|
"%Y/%m/%d",
|
|
331
345
|
), # end date of the supply, tomorrow if unset
|
|
332
346
|
# the following parameters are not crucial, so they can be none
|
|
333
|
-
address=i
|
|
334
|
-
postal_code=i
|
|
335
|
-
province=i
|
|
336
|
-
municipality=i
|
|
337
|
-
distributor=i
|
|
347
|
+
address=i.get("address", None),
|
|
348
|
+
postal_code=i.get("postalCode", None),
|
|
349
|
+
province=i.get("province", None),
|
|
350
|
+
municipality=i.get("municipality", None),
|
|
351
|
+
distributor=i.get("distributor", None),
|
|
338
352
|
# these two are mandatory, we will use them to fetch contracts data
|
|
339
353
|
pointType=i["pointType"],
|
|
340
354
|
distributorCode=i["distributorCode"],
|
|
@@ -355,7 +369,7 @@ class DatadisConnector:
|
|
|
355
369
|
if authorized_nif is not None:
|
|
356
370
|
data["authorizedNif"] = authorized_nif
|
|
357
371
|
response = self._get(
|
|
358
|
-
URL_GET_CONTRACT_DETAIL, request_data=data, ignore_recent_queries=
|
|
372
|
+
URL_GET_CONTRACT_DETAIL, request_data=data, ignore_recent_queries=True
|
|
359
373
|
)
|
|
360
374
|
contracts = []
|
|
361
375
|
tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
|
|
@@ -373,12 +387,16 @@ class DatadisConnector:
|
|
|
373
387
|
),
|
|
374
388
|
marketer=i["marketer"],
|
|
375
389
|
distributorCode=distributor_code,
|
|
376
|
-
power_p1=
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
390
|
+
power_p1=(
|
|
391
|
+
i["contractedPowerkW"][0]
|
|
392
|
+
if isinstance(i["contractedPowerkW"], list)
|
|
393
|
+
else None
|
|
394
|
+
),
|
|
395
|
+
power_p2=(
|
|
396
|
+
i["contractedPowerkW"][1]
|
|
397
|
+
if (len(i["contractedPowerkW"]) > 1)
|
|
398
|
+
else None
|
|
399
|
+
),
|
|
382
400
|
)
|
|
383
401
|
)
|
|
384
402
|
else:
|
|
@@ -53,7 +53,7 @@ class EdataHelper:
|
|
|
53
53
|
self.attributes = {}
|
|
54
54
|
self._storage_dir = storage_dir_path
|
|
55
55
|
self._cups = cups
|
|
56
|
-
self._scups = cups[-5:
|
|
56
|
+
self._scups = cups[-5:]
|
|
57
57
|
self._authorized_nif = datadis_authorized_nif
|
|
58
58
|
self.last_update = {x: datetime(1970, 1, 1) for x in self.data}
|
|
59
59
|
self._date_from = datetime(1970, 1, 1)
|
|
@@ -74,9 +74,11 @@ class EdataHelper:
|
|
|
74
74
|
self.datadis_api = DatadisConnector(
|
|
75
75
|
datadis_username,
|
|
76
76
|
datadis_password,
|
|
77
|
-
storage_path=
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
storage_path=(
|
|
78
|
+
os.path.join(storage_dir_path, const.PROG_NAME)
|
|
79
|
+
if storage_dir_path is not None
|
|
80
|
+
else None
|
|
81
|
+
),
|
|
80
82
|
)
|
|
81
83
|
self.redata_api = REDataConnector()
|
|
82
84
|
|
|
@@ -306,7 +308,7 @@ class EdataHelper:
|
|
|
306
308
|
),
|
|
307
309
|
)
|
|
308
310
|
_LOGGER.info(
|
|
309
|
-
"%s
|
|
311
|
+
"%s: missing maximeter: %s",
|
|
310
312
|
self._scups,
|
|
311
313
|
", ".join(
|
|
312
314
|
[
|
|
@@ -679,11 +681,11 @@ class EdataHelper:
|
|
|
679
681
|
for x in self.data["consumptions"]
|
|
680
682
|
if x["datetime"] >= new_data_from
|
|
681
683
|
],
|
|
682
|
-
prices=
|
|
683
|
-
x for x in self.data["pvpc"] if x["datetime"] >= new_data_from
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
684
|
+
prices=(
|
|
685
|
+
[x for x in self.data["pvpc"] if x["datetime"] >= new_data_from]
|
|
686
|
+
if self.is_pvpc
|
|
687
|
+
else None
|
|
688
|
+
),
|
|
687
689
|
rules=self.pricing_rules,
|
|
688
690
|
)
|
|
689
691
|
)
|
e-data-1.2.16/PKG-INFO
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: e-data
|
|
3
|
-
Version: 1.2.16
|
|
4
|
-
Summary: Python library for managing spanish energy data from various web providers
|
|
5
|
-
Home-page: https://github.com/uvejota/python-edata
|
|
6
|
-
Author: VMG
|
|
7
|
-
Author-email: vmayorg@outlook.es
|
|
8
|
-
License: GPLv3
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
-
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
18
|
-
Requires-Python: >=3.6.0
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[](https://pepy.tech/project/e-data)
|
|
24
|
-
[](https://pepy.tech/project/e-data)
|
|
25
|
-
[](https://pepy.tech/project/e-data)
|
|
26
|
-
|
|
27
|
-
# python-edata
|
|
28
|
-
|
|
29
|
-
Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
|
|
30
|
-
|
|
31
|
-
_**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
|
|
32
|
-
|
|
33
|
-
## Instalación
|
|
34
|
-
|
|
35
|
-
Puedes instalar la última versión estable mediante:
|
|
36
|
-
|
|
37
|
-
``` bash
|
|
38
|
-
pip install e-data
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
|
|
42
|
-
|
|
43
|
-
``` bash
|
|
44
|
-
pip install -r requirements.txt
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Estructura
|
|
48
|
-
|
|
49
|
-
El paquete consta de tres módulos diferenciados:
|
|
50
|
-
|
|
51
|
-
* **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
|
|
52
|
-
* **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
|
|
53
|
-
* **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
|
|
54
|
-
|
|
55
|
-
Estos módulos corresponden a la siguiente estructura del paquete:
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
edata/
|
|
59
|
-
· __init__.py
|
|
60
|
-
· connectors/
|
|
61
|
-
· __init__.py
|
|
62
|
-
· datadis.py
|
|
63
|
-
· redata.py
|
|
64
|
-
· processors/
|
|
65
|
-
· __init__.py
|
|
66
|
-
· base.py
|
|
67
|
-
· billing.py
|
|
68
|
-
· consumption.py
|
|
69
|
-
· maximeter.py
|
|
70
|
-
· utils.py
|
|
71
|
-
· helpers.py
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Ejemplo de uso
|
|
75
|
-
|
|
76
|
-
Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
|
|
77
|
-
* No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
|
|
78
|
-
* El username suele ser el NIF del titular
|
|
79
|
-
* Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
|
|
80
|
-
* La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
|
|
81
|
-
|
|
82
|
-
``` python
|
|
83
|
-
from datetime import datetime
|
|
84
|
-
import json
|
|
85
|
-
|
|
86
|
-
# importamos definiciones de datos que nos interesen
|
|
87
|
-
from edata.definitions import PricingRules
|
|
88
|
-
# importamos el ayudante
|
|
89
|
-
from edata.helpers import EdataHelper
|
|
90
|
-
# importamos el procesador de utilidades
|
|
91
|
-
from edata.processors import utils
|
|
92
|
-
|
|
93
|
-
# Preparar reglas de tarificación (si se quiere)
|
|
94
|
-
PRICING_RULES_PVPC = PricingRules(
|
|
95
|
-
p1_kw_year_eur=30.67266,
|
|
96
|
-
p2_kw_year_eur=1.4243591,
|
|
97
|
-
meter_month_eur=0.81,
|
|
98
|
-
market_kw_year_eur=3.113,
|
|
99
|
-
electricity_tax=1.0511300560,
|
|
100
|
-
iva_tax=1.05,
|
|
101
|
-
# podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
|
|
102
|
-
p1_kwh_eur=None,
|
|
103
|
-
p2_kwh_eur=None,
|
|
104
|
-
p3_kwh_eur=None,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Instanciar el helper
|
|
108
|
-
# 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
|
|
109
|
-
# 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
|
|
110
|
-
edata = EdataHelper(
|
|
111
|
-
"datadis_user",
|
|
112
|
-
"datadis_password",
|
|
113
|
-
"cups",
|
|
114
|
-
datadis_authorized_nif=None,
|
|
115
|
-
pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
|
|
116
|
-
data=None, # aquí podríamos cargar datos anteriores
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Solicitar actualización de todo el histórico (se almacena en edata.data)
|
|
120
|
-
edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
|
|
121
|
-
|
|
122
|
-
# volcamos todo lo obtenido a un fichero
|
|
123
|
-
with open("backup.json", "w") as file:
|
|
124
|
-
json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
|
|
125
|
-
|
|
126
|
-
# Imprimir atributos
|
|
127
|
-
print(edata.attributes)
|
|
128
|
-
```
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: e-data
|
|
3
|
-
Version: 1.2.16
|
|
4
|
-
Summary: Python library for managing spanish energy data from various web providers
|
|
5
|
-
Home-page: https://github.com/uvejota/python-edata
|
|
6
|
-
Author: VMG
|
|
7
|
-
Author-email: vmayorg@outlook.es
|
|
8
|
-
License: GPLv3
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
-
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
-
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
18
|
-
Requires-Python: >=3.6.0
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
[](https://pepy.tech/project/e-data)
|
|
24
|
-
[](https://pepy.tech/project/e-data)
|
|
25
|
-
[](https://pepy.tech/project/e-data)
|
|
26
|
-
|
|
27
|
-
# python-edata
|
|
28
|
-
|
|
29
|
-
Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
|
|
30
|
-
|
|
31
|
-
_**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
|
|
32
|
-
|
|
33
|
-
## Instalación
|
|
34
|
-
|
|
35
|
-
Puedes instalar la última versión estable mediante:
|
|
36
|
-
|
|
37
|
-
``` bash
|
|
38
|
-
pip install e-data
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
|
|
42
|
-
|
|
43
|
-
``` bash
|
|
44
|
-
pip install -r requirements.txt
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Estructura
|
|
48
|
-
|
|
49
|
-
El paquete consta de tres módulos diferenciados:
|
|
50
|
-
|
|
51
|
-
* **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
|
|
52
|
-
* **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
|
|
53
|
-
* **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
|
|
54
|
-
|
|
55
|
-
Estos módulos corresponden a la siguiente estructura del paquete:
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
edata/
|
|
59
|
-
· __init__.py
|
|
60
|
-
· connectors/
|
|
61
|
-
· __init__.py
|
|
62
|
-
· datadis.py
|
|
63
|
-
· redata.py
|
|
64
|
-
· processors/
|
|
65
|
-
· __init__.py
|
|
66
|
-
· base.py
|
|
67
|
-
· billing.py
|
|
68
|
-
· consumption.py
|
|
69
|
-
· maximeter.py
|
|
70
|
-
· utils.py
|
|
71
|
-
· helpers.py
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Ejemplo de uso
|
|
75
|
-
|
|
76
|
-
Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
|
|
77
|
-
* No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
|
|
78
|
-
* El username suele ser el NIF del titular
|
|
79
|
-
* Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
|
|
80
|
-
* La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
|
|
81
|
-
|
|
82
|
-
``` python
|
|
83
|
-
from datetime import datetime
|
|
84
|
-
import json
|
|
85
|
-
|
|
86
|
-
# importamos definiciones de datos que nos interesen
|
|
87
|
-
from edata.definitions import PricingRules
|
|
88
|
-
# importamos el ayudante
|
|
89
|
-
from edata.helpers import EdataHelper
|
|
90
|
-
# importamos el procesador de utilidades
|
|
91
|
-
from edata.processors import utils
|
|
92
|
-
|
|
93
|
-
# Preparar reglas de tarificación (si se quiere)
|
|
94
|
-
PRICING_RULES_PVPC = PricingRules(
|
|
95
|
-
p1_kw_year_eur=30.67266,
|
|
96
|
-
p2_kw_year_eur=1.4243591,
|
|
97
|
-
meter_month_eur=0.81,
|
|
98
|
-
market_kw_year_eur=3.113,
|
|
99
|
-
electricity_tax=1.0511300560,
|
|
100
|
-
iva_tax=1.05,
|
|
101
|
-
# podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
|
|
102
|
-
p1_kwh_eur=None,
|
|
103
|
-
p2_kwh_eur=None,
|
|
104
|
-
p3_kwh_eur=None,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Instanciar el helper
|
|
108
|
-
# 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
|
|
109
|
-
# 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
|
|
110
|
-
edata = EdataHelper(
|
|
111
|
-
"datadis_user",
|
|
112
|
-
"datadis_password",
|
|
113
|
-
"cups",
|
|
114
|
-
datadis_authorized_nif=None,
|
|
115
|
-
pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
|
|
116
|
-
data=None, # aquí podríamos cargar datos anteriores
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Solicitar actualización de todo el histórico (se almacena en edata.data)
|
|
120
|
-
edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
|
|
121
|
-
|
|
122
|
-
# volcamos todo lo obtenido a un fichero
|
|
123
|
-
with open("backup.json", "w") as file:
|
|
124
|
-
json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
|
|
125
|
-
|
|
126
|
-
# Imprimir atributos
|
|
127
|
-
print(edata.attributes)
|
|
128
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|