masstack-python-client 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ __version__ = "0.0.1"
2
+ __homepage__ = "https://somconnexio.coop/"
3
+ __author__ = "Borja Gimeno <borja.gimeno@somconnexio.coop>"
4
+ __license__ = "GPL-3.0-only"
File without changes
@@ -0,0 +1,11 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timedelta
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class Token:
7
+ access_token: str
8
+ expires_at: datetime
9
+
10
+ def is_expired(self, skew_seconds: int = 60) -> bool:
11
+ return datetime.utcnow() >= self.expires_at - timedelta(seconds=skew_seconds)
@@ -0,0 +1,86 @@
1
+ import os
2
+ import base64
3
+ import requests
4
+ from datetime import datetime, timedelta
5
+ from joserfc import jwt
6
+ from joserfc.jwk import RSAKey
7
+ from requests.exceptions import HTTPError
8
+ from ..exceptions import ErrorFetchingToKen
9
+ from .token import Token
10
+ from .token_store import FileTokenStore
11
+
12
+
13
+ class TokenManager:
14
+
15
+ def __init__(self):
16
+ self._token_uri = self._token_uri()
17
+ self._client_email = self._client_email()
18
+ self._private_key = self._private_key()
19
+ self._private_key_id = self._private_key_id()
20
+ self._store = FileTokenStore(self._store_path())
21
+
22
+ @staticmethod
23
+ def _token_uri() -> str:
24
+ return os.environ.get("MASSTACK_TOKEN_URI", "")
25
+
26
+ @staticmethod
27
+ def _client_email() -> str:
28
+ return os.environ.get("MASSTACK_CLIENT_EMAIL", "")
29
+
30
+ @staticmethod
31
+ def _private_key() -> str:
32
+ base64_private_key = os.environ.get("MASSTACK_PRIVATE_KEY_BASE64", "").encode(
33
+ "ascii"
34
+ )
35
+ base64_decode_bytes_private_key = base64.b64decode(base64_private_key)
36
+ return base64_decode_bytes_private_key.decode("ascii")
37
+
38
+ @staticmethod
39
+ def _private_key_id() -> str:
40
+ return os.environ.get("MASSTACK_PRIVATE_KEY_ID", "")
41
+
42
+ @staticmethod
43
+ def _store_path() -> str:
44
+ return os.environ.get("MASSTACK_TOKEN_STORE_PATH")
45
+
46
+ def _fetch_token(self) -> Token:
47
+ now = datetime.utcnow()
48
+ claims = {
49
+ "iss": self._client_email,
50
+ "aud": self._token_uri,
51
+ "exp": now + timedelta(hours=1),
52
+ "iat": now,
53
+ }
54
+ jwt_header = {"alg": "RS256", "kid": self._private_key_id}
55
+
56
+ try:
57
+ key = RSAKey.import_key(self._private_key)
58
+ assertion = jwt.encode(jwt_header, claims, key)
59
+ response = requests.post(
60
+ self._token_uri,
61
+ data={
62
+ "assertion": assertion,
63
+ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
64
+ },
65
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
66
+ )
67
+ response.raise_for_status()
68
+ except HTTPError:
69
+ raise ErrorFetchingToKen(response.status_code, response.text)
70
+ except Exception as e:
71
+ raise ErrorFetchingToKen(500, str(e))
72
+ data = response.json()
73
+
74
+ return Token(
75
+ access_token=data["access_token"],
76
+ expires_at=datetime.utcnow() + timedelta(seconds=data["expires_in"]),
77
+ )
78
+
79
+ def get(self) -> str:
80
+ token = self._store.load()
81
+
82
+ if token is None or token.is_expired():
83
+ token = self._fetch_token()
84
+ self._store.save(token)
85
+
86
+ return token.access_token
@@ -0,0 +1,34 @@
1
+ import json
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ import tempfile
6
+ from .token import Token
7
+
8
+
9
+ class FileTokenStore:
10
+
11
+ def __init__(self, path: Optional[str] = None):
12
+ self.path = Path(
13
+ path if path is not None else Path(tempfile.gettempdir()) / "masstack.token"
14
+ )
15
+
16
+ def load(self) -> Optional[Token]:
17
+ if not self.path.exists():
18
+ return None
19
+
20
+ data = json.loads(self.path.read_text())
21
+ return Token(
22
+ access_token=data["access_token"],
23
+ expires_at=datetime.fromisoformat(data["expires_at"]),
24
+ )
25
+
26
+ def save(self, token: Token) -> None:
27
+ self.path.write_text(
28
+ json.dumps(
29
+ {
30
+ "access_token": token.access_token,
31
+ "expires_at": token.expires_at.isoformat(),
32
+ }
33
+ )
34
+ )
@@ -0,0 +1,85 @@
1
+ import os
2
+ import requests as req
3
+ from typing import Optional
4
+ from masstack_python_client.auth.token_manager import TokenManager
5
+ from masstack_python_client.exceptions import ErrorPostingData, ErrorRetrievingData
6
+
7
+
8
+ class MasstackClient:
9
+ """Base Masstack API client"""
10
+
11
+ def __init__(self):
12
+ self._token_manager = TokenManager()
13
+ self.domain = self._domain()
14
+ self.org_id = self._org_id()
15
+
16
+ @staticmethod
17
+ def _org_id() -> str:
18
+ return os.environ.get("MASSTACK_ORG_ID", "")
19
+
20
+ @staticmethod
21
+ def _domain() -> str:
22
+ return os.environ.get("MASSTACK_DOMAIN", "")
23
+
24
+ def _common_header(self) -> dict:
25
+ token = self._token_manager.get()
26
+ return {"Authorization": f"Bearer {token}", "Accept": "application/json"}
27
+
28
+ def get(
29
+ self,
30
+ api: str,
31
+ version: str,
32
+ endpoint: str,
33
+ params: Optional[dict] = None,
34
+ header: Optional[dict] = None,
35
+ data: Optional[dict] = None,
36
+ ) -> dict:
37
+ url = self._build_url(api, version, endpoint)
38
+ res = self._send_request("GET", url, extra_headers=header, params=params)
39
+
40
+ if res.status_code >= 400:
41
+ raise ErrorRetrievingData(res.status_code, res.text, params)
42
+
43
+ return res.json()
44
+
45
+ def post(
46
+ self,
47
+ api: str,
48
+ version: str,
49
+ endpoint: str,
50
+ data: Optional[dict],
51
+ header: Optional[dict] = None,
52
+ params: Optional[dict] = None,
53
+ ) -> dict:
54
+ url = self._build_url(api, version, endpoint)
55
+ res = self._send_request(
56
+ "POST", url, extra_headers=header, payload=data, params=params
57
+ )
58
+ if res.status_code >= 400:
59
+ raise ErrorPostingData(res.status_code, res.text, data)
60
+
61
+ return res.json()
62
+
63
+ def _send_request(
64
+ self,
65
+ verb: str,
66
+ url: str,
67
+ payload: Optional[dict] = None,
68
+ params: Optional[dict] = None,
69
+ extra_headers: Optional[dict] = None,
70
+ ) -> req.Response:
71
+ headers = self._common_header()
72
+ if extra_headers:
73
+ headers.update(extra_headers)
74
+
75
+ return req.request(
76
+ verb.upper(), url, headers=headers, json=payload, params=params
77
+ )
78
+
79
+ def _build_url(self, api: str, version: str, endpoint: str) -> str:
80
+ return (
81
+ f"https://{api}.{self.domain}/"
82
+ f"{version.strip('/')}/"
83
+ f"orgs/{self.org_id.strip('/')}/"
84
+ f"{endpoint.lstrip('/')}"
85
+ )
@@ -0,0 +1,51 @@
1
+ import csv
2
+ from pathlib import Path
3
+
4
+ BASE_DIR = Path(__file__).resolve().parent
5
+ PROVINCE_FILE_PATH = BASE_DIR / "province_translation.csv"
6
+ STREET_TYPE_FILE_PATH = BASE_DIR / "street_type_translation.csv"
7
+ UNIT_TRANSLATION_FILE_PATH = BASE_DIR / "unit_translation.csv"
8
+
9
+
10
+ def get_province_by_id(
11
+ province_id: str, province: str = "", lang: str = "ca_ES"
12
+ ) -> str:
13
+ with open(
14
+ PROVINCE_FILE_PATH,
15
+ newline="",
16
+ encoding="utf-8",
17
+ ) as f:
18
+ return _get_by_code(csv.DictReader(f), province_id, lang, province)
19
+
20
+
21
+ def get_street_type(original_text: str, lang: str = "ca_ES") -> str:
22
+ with open(
23
+ STREET_TYPE_FILE_PATH,
24
+ newline="",
25
+ encoding="utf-8",
26
+ ) as f:
27
+ return _get_translation(csv.DictReader(f), original_text, lang)
28
+
29
+
30
+ def get_unit_translation(original_text: str, lang: str = "ca_ES") -> str:
31
+ with open(
32
+ UNIT_TRANSLATION_FILE_PATH,
33
+ newline="",
34
+ encoding="utf-8",
35
+ ) as f:
36
+ return _get_translation(csv.DictReader(f), original_text, lang)
37
+
38
+
39
+ def _get_by_code(reader, code, lang, default):
40
+ return next((row[lang] for row in reader if row["code"] == code), default)
41
+
42
+
43
+ def _get_translation(reader, text, lang):
44
+ match = {"es": None, "ca": None}
45
+ for row in reader:
46
+ if row["es_ES"] == text:
47
+ match["es"] = row
48
+ if row["ca_ES"] == text:
49
+ match["ca"] = row
50
+
51
+ return ((m := match["es"] or match["ca"]) and m.get(lang)) or text
@@ -0,0 +1,53 @@
1
+ code,es_ES,ca_ES
2
+ 1,ALAVA,ALAVA
3
+ 2,ALBACETE,ALBACETE
4
+ 3,ALICANTE,ALACANT
5
+ 4,ALMERIA,ALMERIA
6
+ 33,ASTURIAS,ASTURIES
7
+ 5,AVILA,AVILA
8
+ 6,BADAJOZ,BADAJOZ
9
+ 8,BARCELONA,BARCELONA
10
+ 9,BURGOS,BURGOS
11
+ 10,CACERES,CACERES
12
+ 11,CADIZ,CADIZ
13
+ 39,CANTABRIA,CANTABRIA
14
+ 12,CASTELLON,CASTELLO
15
+ 51,CEUTA,CEUTA
16
+ 13,CIUDAD REAL,CIUTAT REAL
17
+ 14,CORDOBA,CORDOBA
18
+ 15,CORU+ANE-A (A),CORUNYA (A)
19
+ 16,CUENCA,CUENCA
20
+ 17,GERONA,GIRONA
21
+ 18,GRANADA,GRANADA
22
+ 19,GUADALAJARA,GUADALAJARA
23
+ 20,GUIPUZCOA,GIPUSKOA
24
+ 21,HUELVA,HUELVA
25
+ 22,HUESCA,HUESCA
26
+ 7,ILLES BALEARS,ILLES BALEARS
27
+ 23,JAEN,JAEN
28
+ 24,LEON,LEON
29
+ 25,LERIDA,LLEIDA
30
+ 27,LUGO,LUGO
31
+ 28,MADRID,MADRID
32
+ 29,MALAGA,MALAGA
33
+ 52,MELILLA,MELILLA
34
+ 30,MURCIA,MURCIA
35
+ 31,NAVARRA,NAVARRA
36
+ 32,OURENSE,ORENSE
37
+ 34,PALENCIA,PALENCIA
38
+ 35,PALMAS (LAS),PALMES (LES)
39
+ 36,PONTEVEDRA,PONTEVEDRA
40
+ 26,RIOJA (LA),RIOJA (LA)
41
+ 37,SALAMANCA,SALAMANCA
42
+ 38,SANTA CRUZ DE TENERIFE,SANTA CRUZ DE TENERIFE
43
+ 40,SEGOVIA,SEGOVIA
44
+ 41,SEVILLA,SEVILLA
45
+ 42,SORIA,SORIA
46
+ 43,TARRAGONA,TARRAGONA
47
+ 44,TERUEL,TEROL
48
+ 45,TOLEDO,TOLEDO
49
+ 46,VALENCIA,VALENCIA
50
+ 47,VALLADOLID,VALLADOLID
51
+ 48,VIZCAYA,BIZKAIA
52
+ 49,ZAMORA,ZAMORA
53
+ 50,ZARAGOZA,SARAGOSSA
@@ -0,0 +1,91 @@
1
+ code,es_ES,ca_ES
2
+ AL,ALAMEDA,ALAMEDA
3
+ AD,ALDEA,ALDEA
4
+ AP,APARTAMENTOS,APARTAMENTS
5
+ AY,ARROYO,RIEROL
6
+ AV,AVENIDA,AVINGUDA
7
+ BJ,BAJADA,BAIXADA
8
+ BR,BARRANCO,BARRANC
9
+ BO,BARRIO,BARRI
10
+ BL,BLOQUE,BLOC
11
+ CL,CALLE,CARRER
12
+ CJ,CALLEJA,CARRERÓ
13
+ CM,CAMINO,CAMÍ
14
+ CR,CARRETERA,CARRETERA
15
+ CS,CASERIO,CASERIU
16
+ CH,CHALET,XALÉ
17
+ CG,COLEGIO,COL·LEGI
18
+ CO,COLONIA,COLÒNIA
19
+ CN,CONJUNTO,CONJUNT
20
+ CT,CUESTA,COSTA
21
+ ED,EDIFICIO,EDIFICI
22
+ EN,ENTRADA,ENTRADA
23
+ ES,ESCALINATA,ESCALINATA
24
+ EX,EXPLANADA,ESPLANADA
25
+ EM,EXTRAMUROS,EXTRAMURS
26
+ ER,EXTRARRADIO,EXTRARADI
27
+ FC,FERROCARRIL,FERROCARRIL
28
+ GL,GLORIETA,GLORIETA
29
+ GV,GRAN VÍA,GRAN VIA
30
+ GR,GRUPO,GRUP
31
+ HT,HUERTA,HORTA
32
+ JR,JARDINES,JARDINS
33
+ LD,LADO,COSTAT
34
+ LG,LUGAR,LLOC
35
+ MZ,MANZANA,ILLA
36
+ MS,MASÍA,MASIA
37
+ MC,MERCADO,MERCAT
38
+ MT,MONTE,MUNTANYA
39
+ ML,MUELLE,MOLL
40
+ MN,MUNICIPIO,MUNICIPI
41
+ PA,PARCELA,PARCEL·LA
42
+ PQ,PARQUE,PARC
43
+ PI,PARROQUIA,PARRÒQUIA
44
+ PD,PARTIDA,PARTIDA
45
+ PJ,PASAJE,PASSATGE
46
+ PS,PASEO,PASSEIG
47
+ PZ,PLAZA,PLAÇA
48
+ PB,POBLADO,POBLAT
49
+ PG,POLIGONO,POLÍGON
50
+ PR,PROLONGACION,PROLONGACIÓ
51
+ PT,PUENTE,PONT
52
+ PU,PUERTA,PORTA
53
+ QT,QUINTA,QUINTA
54
+ RM,RAMAL,RAMAL
55
+ RB,RAMBLA,RAMBLA
56
+ RP,RAMPA,RAMPA
57
+ RR,RIERA,RIERA
58
+ RC,RINCÓN,RACÓ
59
+ RD,RONDA,RONDA
60
+ RU,RÚA,RUA
61
+ SA,SALIDA,SORTIDA
62
+ SC,SECTOR,SECTOR
63
+ SC,SECCIÓN,SECCIÓ
64
+ SD,SENDA,SENDERA
65
+ SL,SOLAR,SOLAR
66
+ SL,SALÓN,SALÓ
67
+ SB,SUBIDA,PUJADA
68
+ TN,TERRENOS,TERRENYS
69
+ TO,TORRENTE,TORRENT
70
+ TR,TRAVESÍA,TRAVESSERA
71
+ UR,URBANIZACION,URBANITZACIÓ
72
+ VI,VÍA,VIA
73
+ VP,VÍA PÚBLICA,VIA PÚBLICA
74
+ AR,ÁREA,ÀREA
75
+ AR,ARRABAL,RAVAL
76
+ AC,ACCESO,ACCÉS
77
+ VR,ACERA,VORERA
78
+ AQ,ACEQUIA,SÈQUIA
79
+ AF,AFUERA(S),AFORES
80
+ AG,AGRUPACION,AGRUPACIÓ
81
+ AS,ARRABAL(ES),RAVALS
82
+ AV,AUTOPISTA/AUTOVIA,AUTOPISTA/AUTOVIA
83
+ BB,BARRIADA/BAJADA,BARRIADA/BAIXADA
84
+ CC,CASA(S)/CASETA,CASA(ES)/CASETA
85
+ MJ,COMPLEJO,COMPLEXE
86
+ RJ,CORTIJO,MAS
87
+ DM,DISEMINADO(S)/EXTR.,DISSEMINAT(S)/EXTR.
88
+ NO,NUCLEO,NUCLI
89
+ PO,PATIO,PATI
90
+ LA,POBLACIÓN,POBLACIÓ
91
+ RS,RESIDENCIA,RESIDÈNCIA
@@ -0,0 +1,13 @@
1
+ es_ES,ca_ES
2
+ BAJO,BAIX
3
+ LOCAL,LOCAL
4
+ OTROS,ALTRES
5
+ ATICO,ATIC
6
+ PLANTA BAJA,PLANTA BAIXA
7
+ NAVE,NAU
8
+ PARCELA,PARCEL·LA
9
+ ENTRESUELO,ENTRESOL
10
+ BLOQUE,BLOC
11
+ TIENDA,TENDA
12
+ SOTANO,SOTERRANI
13
+ SOBREATICO,SOBREATIC
@@ -0,0 +1,57 @@
1
+ from typing import Optional
2
+
3
+
4
+ class MasstackClientException(Exception):
5
+ """Base exception for Masstack Python Client."""
6
+
7
+ _status_code: Optional[int] = None
8
+ _message: Optional[str] = None
9
+
10
+ def __init__(self, status_code: int, message: str):
11
+ super(Exception, self).__init__(message)
12
+ self._message = message
13
+ self._status_code = status_code
14
+
15
+ @property
16
+ def status_code(self) -> int:
17
+ """The status_code property."""
18
+ return self._status_code or 0
19
+
20
+ @property
21
+ def message(self) -> str:
22
+ """The error message property."""
23
+ return self._message or ""
24
+
25
+ def _params_to_string(self, params):
26
+ if not params or len(params) == 0:
27
+ return ""
28
+ params_msg = ""
29
+ for key, value in params.items():
30
+ params_msg = params_msg + "{}: {}\n".format(key, value)
31
+ return params_msg
32
+
33
+
34
+ class ErrorFetchingToKen(MasstackClientException):
35
+ def __init__(self, status_code: int, error_msg: str):
36
+ message = "Error fetching token with the next error message: {}".format(
37
+ error_msg
38
+ )
39
+ super(ErrorFetchingToKen, self).__init__(status_code, message)
40
+
41
+
42
+ class ErrorRetrievingData(MasstackClientException):
43
+ def __init__(self, status_code: int, error_msg: str, params: Optional[dict] = None):
44
+ message = "Error retrieving data with the next error message: {}\nParameters:\n{}".format(
45
+ error_msg, self._params_to_string(params)
46
+ )
47
+ super(ErrorRetrievingData, self).__init__(status_code, message)
48
+
49
+
50
+ class ErrorPostingData(MasstackClientException):
51
+ def __init__(self, status_code: int, error_msg: str, body: dict):
52
+ message = (
53
+ "Error posting data with the next error message: {}\nBody:\n{}".format(
54
+ error_msg, self._params_to_string(body)
55
+ )
56
+ )
57
+ super(ErrorPostingData, self).__init__(status_code, message)
File without changes