emudb-api-py 0.1.0__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,3 @@
1
+ from emudb_api_py.core import EmuAPIConnector
2
+
3
+ __all__ = ["EmuAPIConnector"]
emudb_api_py/core.py ADDED
@@ -0,0 +1,69 @@
1
+ import json
2
+ from contextlib import contextmanager
3
+ from datetime import datetime, timedelta
4
+
5
+ import requests
6
+
7
+ from emudb_api_py.sessions.core import (
8
+ BaseSession,
9
+ EmuAPICore,
10
+ EmuApiError,
11
+ EmuAPIUrl,
12
+ EmuToken,
13
+ )
14
+
15
+
16
+ class EmuAPIConnector(EmuAPICore):
17
+ def __init__(
18
+ self,
19
+ host: str,
20
+ user: str,
21
+ pwd: str,
22
+ tenant: str,
23
+ ) -> None:
24
+ self._tenant = tenant
25
+ self._url = EmuAPIUrl(f"{host}/{self._tenant}")
26
+ self._user = user
27
+ self._pwd = pwd
28
+
29
+ @property
30
+ def _creds(self) -> str:
31
+ return json.dumps(
32
+ {
33
+ "username": self._user,
34
+ "password": self._pwd,
35
+ "timeout": 30,
36
+ # We handle token renewal ourselves so we don't have to update the
37
+ # cached token after every request
38
+ "renew": False,
39
+ }
40
+ )
41
+
42
+ @contextmanager
43
+ def session(self):
44
+ token = self._signin()
45
+ yield BaseSession(self._url, token)
46
+ self._signout(token)
47
+
48
+ @staticmethod
49
+ def _parse_create_token_resp(resp: requests.Response) -> EmuToken:
50
+ if resp.status_code == 201:
51
+ # Bearer token comes back in the headers:
52
+ token = resp.headers["Authorization"]
53
+ data = json.loads(resp.content)["data"]
54
+ # Subtract 30 seconds from the token expiration so that it doesn't
55
+ # expire during request processing if a request is submitted to the API
56
+ # very close to expiration:
57
+ exp = datetime.fromtimestamp(data["exp"]) - timedelta(seconds=30)
58
+ return EmuToken(value=token, exp=exp)
59
+ else:
60
+ raise EmuApiError(resp.status_code, resp.content)
61
+
62
+ def _signin(self) -> EmuToken:
63
+ resp = requests.post(
64
+ self._url.tokens, data=self._creds, headers=self._gen_headers()
65
+ )
66
+ return self._parse_create_token_resp(resp)
67
+
68
+ def _signout(self, token: EmuToken) -> None:
69
+ requests.delete(self._url.tokens, headers=self._gen_headers(token))
File without changes
@@ -0,0 +1,222 @@
1
+ from abc import ABC, abstractmethod
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Any, Iterable, Mapping, Optional, TypedDict
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class BaseSchema(BaseModel):
10
+ pass
11
+
12
+
13
+ class _EmuSchemaField(TypedDict):
14
+ present: bool
15
+ rest_type: Optional[str]
16
+ new_field: Optional[str]
17
+
18
+
19
+ class EmuAPISchemaProp(TypedDict):
20
+ type: str | list[str]
21
+ format: Optional[str]
22
+ pattern: Optional[str]
23
+ items: Optional[EmuAPISchemaProp]
24
+ properties: Optional[dict[str, EmuAPISchemaProp]]
25
+ additionalProperties: Optional[bool]
26
+
27
+
28
+ class BaseEmuSchema(BaseSchema, ABC):
29
+ irn: int
30
+ AdmDateInserted: str
31
+ AdmTimeInserted: str
32
+ AdmTimeModified: str
33
+ AdmDateModified: str
34
+
35
+ @classmethod
36
+ @abstractmethod
37
+ def get_resource(cls) -> str:
38
+ return NotImplemented
39
+
40
+ @classmethod
41
+ def decode_irn(cls, irnobj: Mapping[str, Any]) -> int:
42
+ id = irnobj["id"]
43
+ return cls.decode_id_uri(id)
44
+
45
+ @staticmethod
46
+ def decode_id_uri(uri: str) -> int:
47
+ try:
48
+ # id uri is in the format: emu:/tenant/resource/123
49
+ return int(uri.split("/")[-1])
50
+ except ValueError:
51
+ raise ValueError(
52
+ f"emu irn object has unexpected format in 'id' field: {id}"
53
+ )
54
+
55
+ @classmethod
56
+ def gen_field_list(
57
+ cls, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None
58
+ ) -> list[str]:
59
+ """
60
+ Generates a list of field names for use when querying the emu REST API.
61
+
62
+ Args:
63
+ include (Iterable[str] | None, optional): Optional list of fields to
64
+ include. Defaults to None, which will provide all available fields
65
+ for the schema.
66
+ exclude (Iterable[str] | None, optional): Optional list of fields to
67
+ exclude from all the available fields. Defaults to None.
68
+
69
+ Returns:
70
+ list[str]: The list of fields associated with this schema.
71
+ """
72
+ fields = [*include] if include is not None else [*cls.__annotations__.keys()]
73
+ if exclude:
74
+ for ex in exclude:
75
+ fields.remove(ex)
76
+ return fields
77
+
78
+ @classmethod
79
+ def gen_field_select(
80
+ cls, include: Iterable[str] | None = None, exclude: Iterable[str] | None = None
81
+ ) -> str:
82
+ # exclude = ["irn"] if exclude is None else {*exclude, "irn"}
83
+ fields = cls.gen_field_list(include, exclude)
84
+ return "id," + ",".join([f"data.{f}" for f in fields])
85
+
86
+
87
+ _API_TYPE_MAP = {
88
+ "string": "str",
89
+ "integer": "int",
90
+ "number": "float",
91
+ }
92
+
93
+
94
+ def assemble_items_type_str(prop: EmuAPISchemaProp) -> str:
95
+ result = ""
96
+ api_type = prop["type"]
97
+ items = prop.get("items")
98
+ if items:
99
+ inner_result = assemble_items_type_str(items)
100
+ result = f"list[{inner_result}]"
101
+ if "null" in api_type:
102
+ result = f"Optional[{result}]"
103
+ elif api_type == "array":
104
+ raise ValueError(
105
+ f"Expected 'array' api type to have 'items' property: ({prop})"
106
+ )
107
+ else:
108
+ result = _API_TYPE_MAP[api_type[0]]
109
+ if "null" in api_type:
110
+ result = f"Optional[{result}]"
111
+ return result
112
+
113
+
114
+ def assemble_class_str(
115
+ name: str,
116
+ props: dict[str, EmuAPISchemaProp],
117
+ level: int = 0,
118
+ ) -> tuple[str, bool]:
119
+ has_subclasses = False
120
+ result = f"{'\t' * level}class "
121
+ result += f"{name}(BaseEmuSchema)" if level == 0 else f"_{name}(BaseSchema)"
122
+ result += ":\n"
123
+ subclasses = list[str]()
124
+ fields = list[str]()
125
+ for field, prop in props.items():
126
+ items = prop.get("items")
127
+ api_type = prop["type"]
128
+ if items:
129
+ subprops = items.get("properties")
130
+ if subprops:
131
+ clsname = field.replace("_grp", "").replace("_subgrp", "")
132
+ has_subclasses = True
133
+ subclass_str, _ = assemble_class_str(clsname, subprops, level + 1)
134
+ subclasses.append(subclass_str)
135
+ _type = f"_{clsname}"
136
+ if isinstance(items["type"], list) and "null" in items["type"]:
137
+ _type = f"Optional[{_type}]"
138
+ if api_type == "array":
139
+ _type = f"list[{_type}]"
140
+ else:
141
+ _type = assemble_items_type_str(prop)
142
+ elif isinstance(api_type, str):
143
+ _type = _API_TYPE_MAP[api_type]
144
+ else:
145
+ raise ValueError(
146
+ f"Unexpected property structure from API ({field}: {prop})"
147
+ )
148
+ fields.append(f"{'\t' * (level + 1)}{field}: Optional[{_type}]\n")
149
+ for subclass in subclasses:
150
+ result += subclass
151
+ for field in fields:
152
+ result += field
153
+ result += "\n"
154
+ return result, has_subclasses
155
+
156
+
157
+ def create_schema_module(
158
+ schema_dir: str | Path, resource: str, schema_class_name: str
159
+ ) -> None:
160
+ schema_dir = Path(schema_dir)
161
+ with open(schema_dir.joinpath(f"{resource}_schema.json")) as file:
162
+ raw: dict[str, EmuAPISchemaProp] = json.loads(file.read())
163
+ for key in BaseEmuSchema.__annotations__.keys():
164
+ if key in raw:
165
+ raw.pop(key)
166
+ class_str, has_subclasses = assemble_class_str(schema_class_name, raw)
167
+ result = ""
168
+ if "Optional" in class_str:
169
+ result += "from typing import Optional\n\n"
170
+ result += "from emudb_api_py.schemas.core import BaseEmuSchema"
171
+ if has_subclasses:
172
+ result += ", BaseSchema"
173
+ result += f"\n\n\n{class_str}"
174
+ with open(f"emudb_api_py/schemas/{resource}.py", "w") as file:
175
+ file.write(result)
176
+
177
+
178
+ def compare_with_rest_schema(
179
+ schema_dir: str | Path, schema: type[BaseEmuSchema]
180
+ ) -> dict[str, _EmuSchemaField]:
181
+ schema_dir = Path(schema_dir)
182
+ with open(schema_dir.joinpath(f"{schema.get_resource()}_schema.json")) as file:
183
+ raw = json.loads(file.read())
184
+ flat_schema = flatten_rest_schema(raw)
185
+ results = {
186
+ field: _EmuSchemaField(present=False, rest_type=None, new_field=None)
187
+ for field in schema.__annotations__.keys()
188
+ }
189
+ for field, fschema in flat_schema.items():
190
+ f = field
191
+ new_field: str | None = None
192
+ comment: str | None = fschema.get("$comment")
193
+ if comment and comment in results:
194
+ f = comment
195
+ new_field = field
196
+ if f in results:
197
+ results[f] = _EmuSchemaField(
198
+ present=True, rest_type=fschema["type"], new_field=new_field
199
+ )
200
+ with open(
201
+ schema_dir.joinpath(f"{schema.get_resource()}_compare.json"), "w"
202
+ ) as file:
203
+ file.write(json.dumps(results))
204
+ return results
205
+
206
+
207
+ def flatten_rest_schema(
208
+ props: dict[str, dict[str, Any]], parent_key: str | None = None
209
+ ) -> dict[str, dict[str, Any]]:
210
+ results = dict[str, dict[str, Any]]()
211
+ for field, fschema in props.items():
212
+ f = f"{parent_key}.{field}" if parent_key else field
213
+ if "items" in fschema:
214
+ items = fschema["items"]
215
+ if "properties" in items:
216
+ flat_props = flatten_rest_schema(items["properties"], f)
217
+ results = {**results, **flat_props}
218
+ else:
219
+ results[f] = fschema
220
+ else:
221
+ results[f] = fschema
222
+ return results