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.
- emudb_api_py/__init__.py +3 -0
- emudb_api_py/core.py +69 -0
- emudb_api_py/schemas/__init__.py +0 -0
- emudb_api_py/schemas/core.py +222 -0
- emudb_api_py/schemas/ecatalogue.py +430 -0
- emudb_api_py/schemas/esites.py +271 -0
- emudb_api_py/sessions/__init__.py +0 -0
- emudb_api_py/sessions/core.py +122 -0
- emudb_api_py/sessions/session.py +34 -0
- emudb_api_py-0.1.0.dist-info/METADATA +20 -0
- emudb_api_py-0.1.0.dist-info/RECORD +13 -0
- emudb_api_py-0.1.0.dist-info/WHEEL +4 -0
- emudb_api_py-0.1.0.dist-info/licenses/LICENSE +235 -0
emudb_api_py/__init__.py
ADDED
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
|