synmax-api-python-client 4.0.0__py3-none-any.whl → 4.2.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.
- synmax/openapi/__init__.py +0 -0
- synmax/openapi/client.py +199 -0
- synmax/openapi/utils.py +101 -0
- {synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/METADATA +1 -1
- {synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/RECORD +7 -5
- test/ip_rate_example.py +0 -61
- {synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/WHEEL +0 -0
- {synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/top_level.txt +0 -0
|
File without changes
|
synmax/openapi/client.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import httpx
|
|
3
|
+
import logging
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing import Any, Dict, Optional, Type, Iterator
|
|
6
|
+
from prance import ResolvingParser
|
|
7
|
+
import time
|
|
8
|
+
import importlib
|
|
9
|
+
|
|
10
|
+
from itertools import islice
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from synmax.openapi.utils import change_signature, get_annotation, get_body_model, get_param_model
|
|
14
|
+
|
|
15
|
+
class OpenAPIClient:
|
|
16
|
+
def __init__(self,
|
|
17
|
+
base_uri: str = "",
|
|
18
|
+
headers: Optional[Dict[str, str]] = None,
|
|
19
|
+
logger: Optional[logging.Logger] = None,
|
|
20
|
+
timeout = 15.0,
|
|
21
|
+
retries = 3,
|
|
22
|
+
retry_sleep = 20,
|
|
23
|
+
):
|
|
24
|
+
self._base_uri = base_uri.rstrip('/')
|
|
25
|
+
self._retries = retries
|
|
26
|
+
self._retry_sleep = retry_sleep
|
|
27
|
+
self._timeout = timeout
|
|
28
|
+
accept_header = {"Accept": "application/x-ndjson"}
|
|
29
|
+
base_headers = headers or {}
|
|
30
|
+
self._client = httpx.Client(
|
|
31
|
+
headers=dict(base_headers, **accept_header),
|
|
32
|
+
timeout=timeout,
|
|
33
|
+
)
|
|
34
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
35
|
+
parser = ResolvingParser(self._spec, backend = 'openapi-spec-validator')
|
|
36
|
+
self._openapi = parser.specification
|
|
37
|
+
self._paths = self._openapi.get("paths", {})
|
|
38
|
+
self._generate_methods()
|
|
39
|
+
self._logger.info(f"Initialized client for {self._base_uri}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _generate_methods(self):
|
|
43
|
+
for path, methods in self._paths.items():
|
|
44
|
+
for method, details in methods.items():
|
|
45
|
+
method_name = details.get("x-method-name", None)
|
|
46
|
+
if method_name is None:
|
|
47
|
+
continue
|
|
48
|
+
param_model = get_param_model(details.get("parameters", []), method_name)
|
|
49
|
+
body_model = get_body_model(details.get("requestBody", {}), method_name)
|
|
50
|
+
full_url = f"{self._base_uri}{path}"
|
|
51
|
+
request_func = self._create_request_function(method, full_url, param_model, body_model)
|
|
52
|
+
request_func.__name__ = method_name
|
|
53
|
+
setattr(self, method_name, request_func.__get__(self))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _dump_stub(cls) -> str:
|
|
58
|
+
if not hasattr(cls, '_spec'):
|
|
59
|
+
message = f"Class '{cls.__name__}' is missing required attribute: '_spec'"
|
|
60
|
+
raise AttributeError(message)
|
|
61
|
+
t = type(cls.__name__, (OpenAPIClient,), {"_spec": cls._spec})
|
|
62
|
+
return t()._get_annotations()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _write_stub(cls):
|
|
67
|
+
module = cls.__module__
|
|
68
|
+
origin = importlib.util.find_spec(module).origin
|
|
69
|
+
outfile = f"{origin}i"
|
|
70
|
+
with open(outfile, "w") as file:
|
|
71
|
+
file.write(cls._dump_stub())
|
|
72
|
+
print(f"wrote class stub to {outfile}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_annotations(self) -> str:
|
|
76
|
+
lines = []
|
|
77
|
+
lines.append("from typing import Literal, Union")
|
|
78
|
+
lines.append("from synmax.openapi.client import Result")
|
|
79
|
+
lines.append("from datetime import date")
|
|
80
|
+
lines.append(f"class {self.__class__.__name__}:")
|
|
81
|
+
|
|
82
|
+
tab = " "
|
|
83
|
+
|
|
84
|
+
for path, methods in self._paths.items():
|
|
85
|
+
for method, details in methods.items():
|
|
86
|
+
method_name = details.get("x-method-name", details.get("operationId", None))
|
|
87
|
+
if method_name is None:
|
|
88
|
+
continue
|
|
89
|
+
param_model = get_param_model(details.get("parameters", []), method_name)
|
|
90
|
+
body_model = get_body_model(details.get("requestBody", {}), method_name)
|
|
91
|
+
annotation = get_annotation([param_model, body_model])
|
|
92
|
+
description = details.get("summary", None)
|
|
93
|
+
if description is None:
|
|
94
|
+
description = " "
|
|
95
|
+
else:
|
|
96
|
+
description = f"\n{tab}{tab}\"\"\"{description}\"\"\"\n{tab}{tab}"
|
|
97
|
+
|
|
98
|
+
lines.append(f"{tab}def {method_name}({annotation}) -> Result:{description}...")
|
|
99
|
+
return "\n".join(lines)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _create_request_function(self, method: str, full_url: str, param_model: Optional[Type[BaseModel]], body_model: Optional[Type[BaseModel]]):
|
|
103
|
+
def request_func(self, **kwargs) -> Result:
|
|
104
|
+
def generator() -> Iterator[Dict[str, Any]]:
|
|
105
|
+
param_dict = None
|
|
106
|
+
if param_model:
|
|
107
|
+
data = param_model(**kwargs).model_dump(mode='json')
|
|
108
|
+
param_dict = {k: v for k, v in data.dict().items() if v is not None}
|
|
109
|
+
body_dict = None
|
|
110
|
+
if body_model:
|
|
111
|
+
data = body_model(**kwargs).model_dump(mode='json')
|
|
112
|
+
body_dict = {k: v for k, v in data.items() if v is not None}
|
|
113
|
+
self._logger.info(f"Making request to {full_url}")
|
|
114
|
+
retry_count = 0
|
|
115
|
+
can_retry = True
|
|
116
|
+
while can_retry:
|
|
117
|
+
try:
|
|
118
|
+
with self._client.stream(
|
|
119
|
+
method.upper(),
|
|
120
|
+
full_url,
|
|
121
|
+
params=param_dict if param_model else None,
|
|
122
|
+
json=body_dict if body_model else None,
|
|
123
|
+
) as response:
|
|
124
|
+
if response.is_error:
|
|
125
|
+
for chunk in response.iter_text():
|
|
126
|
+
self._logger.error(f"Error response fom api server. Status code: {response.status_code}")
|
|
127
|
+
self._logger.error(chunk)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
total_bytes = 0
|
|
130
|
+
total_records = 0
|
|
131
|
+
last_message = time.time()
|
|
132
|
+
buffer = ""
|
|
133
|
+
for chunk in response.iter_bytes():
|
|
134
|
+
total_bytes += len(chunk)
|
|
135
|
+
buffer += chunk.decode("utf-8")
|
|
136
|
+
while "\n" in buffer:
|
|
137
|
+
line, buffer = buffer.split("\n", 1)
|
|
138
|
+
if line.strip():
|
|
139
|
+
can_retry = False # if we get any response, interrupt retry logic
|
|
140
|
+
yield json.loads(line)
|
|
141
|
+
total_records += 1
|
|
142
|
+
now = time.time()
|
|
143
|
+
if now - last_message >= 1:
|
|
144
|
+
last_message = now
|
|
145
|
+
self._logger.debug(f"Got {total_records} total records, {total_bytes} total bytes")
|
|
146
|
+
if buffer.strip(): # last record has no newline
|
|
147
|
+
yield json.loads(buffer)
|
|
148
|
+
total_records += 1
|
|
149
|
+
self._logger.debug(f"Got {total_records} total records, {total_bytes} total bytes")
|
|
150
|
+
break
|
|
151
|
+
except httpx.ReadTimeout:
|
|
152
|
+
retry_count += 1
|
|
153
|
+
retry_text = ""
|
|
154
|
+
can_retry = can_retry and retry_count <= self._retries
|
|
155
|
+
if can_retry:
|
|
156
|
+
retry_text = f"Retry {retry_count}/{self._retries}. Sleep {self._retry_sleep}s."
|
|
157
|
+
self._logger.error(f"Request timed out ({self._timeout}s). {retry_text}")
|
|
158
|
+
time.sleep(self._retry_sleep)
|
|
159
|
+
|
|
160
|
+
return Result(generator())
|
|
161
|
+
|
|
162
|
+
signature = {}
|
|
163
|
+
for model in (param_model, body_model):
|
|
164
|
+
if model:
|
|
165
|
+
for field_name, field in model.__annotations__.items():
|
|
166
|
+
is_optional = model.__fields__[field_name].default is None
|
|
167
|
+
signature[field_name] = Optional[field] if is_optional else field
|
|
168
|
+
return change_signature(request_func, signature)
|
|
169
|
+
|
|
170
|
+
def close(self):
|
|
171
|
+
self._client.close()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Result(Iterator[Dict[str, Any]]):
|
|
176
|
+
def __init__(self, generator):
|
|
177
|
+
self._generator = generator
|
|
178
|
+
|
|
179
|
+
def __iter__(self):
|
|
180
|
+
return self._generator
|
|
181
|
+
|
|
182
|
+
def __next__(self):
|
|
183
|
+
return next(self._generator)
|
|
184
|
+
|
|
185
|
+
def df(self, chunk_size = 100000) -> pd.DataFrame:
|
|
186
|
+
def batched():
|
|
187
|
+
it = iter(self)
|
|
188
|
+
while True:
|
|
189
|
+
batch = list(islice(it, chunk_size))
|
|
190
|
+
if not batch:
|
|
191
|
+
break
|
|
192
|
+
yield batch
|
|
193
|
+
chunks = []
|
|
194
|
+
for batch in batched():
|
|
195
|
+
chunks.append(pd.DataFrame(batch))
|
|
196
|
+
df = pd.concat(chunks, ignore_index=True)
|
|
197
|
+
return df
|
|
198
|
+
|
|
199
|
+
# TODO: save_csv(self, path)
|
synmax/openapi/utils.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from pydantic import BaseModel, create_model, Extra
|
|
3
|
+
from typing import Any, Dict, Optional, Type, Union, Literal
|
|
4
|
+
from datetime import date
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def change_signature(func, signature: inspect.Signature):
|
|
8
|
+
"""
|
|
9
|
+
Wraps a func in a new one with a different signature
|
|
10
|
+
|
|
11
|
+
:param func: function to wrap
|
|
12
|
+
:param signature: signature to assign
|
|
13
|
+
"""
|
|
14
|
+
def wrapped(*args, **kwargs):
|
|
15
|
+
bound_arguments = inspect.signature(func).bind(*args, **kwargs)
|
|
16
|
+
bound_arguments.apply_defaults()
|
|
17
|
+
return func(*bound_arguments.args, **bound_arguments.kwargs)
|
|
18
|
+
sig = inspect.signature(func)
|
|
19
|
+
new_params = [
|
|
20
|
+
inspect.Parameter(
|
|
21
|
+
name,
|
|
22
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
23
|
+
annotation=signature.get(name)
|
|
24
|
+
)
|
|
25
|
+
for name in signature
|
|
26
|
+
]
|
|
27
|
+
new_signature = sig.replace(parameters=new_params)
|
|
28
|
+
wrapped.__signature__ = new_signature
|
|
29
|
+
return wrapped
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_annotation(models: list[BaseModel]) -> str:
|
|
33
|
+
params = []
|
|
34
|
+
for model in models:
|
|
35
|
+
if model is None:
|
|
36
|
+
continue
|
|
37
|
+
fields = model.model_fields
|
|
38
|
+
for fname, field in fields.items():
|
|
39
|
+
|
|
40
|
+
annotation = repr(field.annotation)
|
|
41
|
+
if hasattr(field.annotation, "__name__"):
|
|
42
|
+
annotation = field.annotation.__name__
|
|
43
|
+
if hasattr(field.annotation, "__origin__"):
|
|
44
|
+
annotation = str(field.annotation).replace("typing.","")
|
|
45
|
+
default = " = ..." if field.is_required() is False else ""
|
|
46
|
+
params.append(f"{fname}: {annotation}{default}")
|
|
47
|
+
return ",".join(["self"] + params)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_param_model(parameters: list, operation_id: str) -> Optional[Type[BaseModel]]:
|
|
51
|
+
param_fields = {}
|
|
52
|
+
for param in parameters:
|
|
53
|
+
name = param["name"]
|
|
54
|
+
schema = param.get("schema", {"type": "string"})
|
|
55
|
+
required = param.get("required", False)
|
|
56
|
+
param_fields[name] = (map_openapi_type(schema, name), ... if required else None)
|
|
57
|
+
return create_model(f"{operation_id}Params", **param_fields) if param_fields else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_body_model(request_body: dict, operation_id: str) -> Optional[Type[BaseModel]]:
|
|
61
|
+
body_fields = {}
|
|
62
|
+
body_extra = Extra.allow
|
|
63
|
+
if request_body:
|
|
64
|
+
content = request_body.get("content", {}).get("application/json", {})
|
|
65
|
+
schema = content.get("schema", {"type": "object", "properties": {}})
|
|
66
|
+
required_fields = schema.get("required", [])
|
|
67
|
+
for key, value in schema.get("properties", {}).items():
|
|
68
|
+
is_required = key in required_fields
|
|
69
|
+
body_fields[key] = (map_openapi_type(value, key), ... if is_required else None)
|
|
70
|
+
if schema.get("additionalProperties") is False:
|
|
71
|
+
body_extra = Extra.forbid
|
|
72
|
+
return create_model(f"{operation_id}Body", **body_fields, __config__=type("Config", (), {"extra": body_extra})) if body_fields else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def map_openapi_type(schema: Dict[str, Any], name: str) -> Type:
|
|
76
|
+
type_map = {
|
|
77
|
+
"string": str,
|
|
78
|
+
"integer": int,
|
|
79
|
+
"boolean": bool,
|
|
80
|
+
"number": float,
|
|
81
|
+
"array": list,
|
|
82
|
+
"object": dict
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if "enum" in schema:
|
|
86
|
+
return Literal[tuple(schema["enum"])]
|
|
87
|
+
|
|
88
|
+
if "anyOf" in schema:
|
|
89
|
+
types = [map_openapi_type(sub_schema, name) for sub_schema in schema["anyOf"]]
|
|
90
|
+
return Union[tuple(types)]
|
|
91
|
+
|
|
92
|
+
if schema.get("type") == "array":
|
|
93
|
+
items_schema = schema.get("items", {})
|
|
94
|
+
item_type = map_openapi_type(items_schema, name)
|
|
95
|
+
return list[item_type]
|
|
96
|
+
|
|
97
|
+
if schema.get("type") == "string" and schema.get("format") == "date":
|
|
98
|
+
return date
|
|
99
|
+
|
|
100
|
+
return type_map.get(schema.get("type"), Any)
|
|
101
|
+
|
{synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/RECORD
RENAMED
|
@@ -15,14 +15,16 @@ synmax/hyperion/process_api.py,sha256=y3sNiM2X_h-QVRRsrW4VmkABfGKxPqqZGMiYNrpRrK
|
|
|
15
15
|
synmax/hyperion/process_inputs.py,sha256=Dij1FTzdyv0yGvwQNYZ9yJTJ6vBf-9B4P2zmJeCo6UU,10796
|
|
16
16
|
synmax/hyperion/v4/__init__.py,sha256=VpXU-_FP836dXUDcUFYr-0y0-ZrSM8aCtGE97Z2qbS4,55
|
|
17
17
|
synmax/hyperion/v4/hyperion_client.py,sha256=Yo6HVmekIH3L-G6ABjlwdxXhcmCG0pV1HDl5p8O1shM,833
|
|
18
|
+
synmax/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
synmax/openapi/client.py,sha256=fe6npo842Z_9ar2SX7wTFz9Y7Jv9vN03zTumpnb1y64,8600
|
|
20
|
+
synmax/openapi/utils.py,sha256=q6PsmUT0D58_AdOCM0ZNw1CrveiYVF4IQN4J6ticZ4Q,3689
|
|
18
21
|
synmax/theia/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
22
|
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
test/ip_rate_example.py,sha256=UQCrYDYH7mtFCet9xCJJNfT-3G5v_LKdeioEovSwVM4,1634
|
|
21
23
|
test/test_api_client.py,sha256=HFWCu_xBgqQXc9p6k6YmP2gVrdCFwuhoDVqlxpN1Yoc,4713
|
|
22
24
|
test/test_hyperion_client.py,sha256=aCb4bMBXWofa82MInXoU9tJ5wWsg5sBRv7B_CHvkrcg,1380
|
|
23
25
|
test/test_hyperion_e2e.py,sha256=BPhZH7eVQZSsFcvPdonKjmLahnfsLy37Z2DHp0LGeHc,6247
|
|
24
26
|
test/test_hyperion_helpers.py,sha256=R0G7oqKaoz6jLRACODJ1Cf2gA0gaMPQhOZRjKp4a6-s,2006
|
|
25
|
-
synmax_api_python_client-4.
|
|
26
|
-
synmax_api_python_client-4.
|
|
27
|
-
synmax_api_python_client-4.
|
|
28
|
-
synmax_api_python_client-4.
|
|
27
|
+
synmax_api_python_client-4.2.1.dist-info/METADATA,sha256=ffdQduj69sIu9qe9PJ9SgfDwp9mJVf03-qMjoGUcbew,4860
|
|
28
|
+
synmax_api_python_client-4.2.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
29
|
+
synmax_api_python_client-4.2.1.dist-info/top_level.txt,sha256=SAjmDfHlJzUmjoGCT1SnS4qGSLN_ZF-p58UN1yW5kB0,17
|
|
30
|
+
synmax_api_python_client-4.2.1.dist-info/RECORD,,
|
test/ip_rate_example.py
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import plotly.express as px
|
|
2
|
-
import plotly.figure_factory as ff
|
|
3
|
-
|
|
4
|
-
from synmax.hyperion import HyperionApiClient, ApiPayload, add_daily, get_fips
|
|
5
|
-
|
|
6
|
-
"""Install SynMax Python Client"""
|
|
7
|
-
|
|
8
|
-
# pip install --upgrade synmax-api-python-client
|
|
9
|
-
|
|
10
|
-
"""Install other Dependencies"""
|
|
11
|
-
|
|
12
|
-
# pip install plotly-geo plotly geopandas==0.8.1 pyshp shapely
|
|
13
|
-
|
|
14
|
-
"""Query the production data"""
|
|
15
|
-
|
|
16
|
-
access_token = "************************************"
|
|
17
|
-
client = HyperionApiClient(access_token=access_token)
|
|
18
|
-
|
|
19
|
-
payload = ApiPayload(
|
|
20
|
-
start_date="2021-01-01", end_date="2022-09-01", state_code="TX", production_month=2
|
|
21
|
-
)
|
|
22
|
-
df = client.production_by_well(payload)
|
|
23
|
-
# df.to_csv(r'C:\Users\eric\Desktop\TEMP\prod_ip.csv', index=False)
|
|
24
|
-
# df = pd.read_csv(r'C:\Users\eric\Desktop\TEMP\prod_ip.csv')
|
|
25
|
-
|
|
26
|
-
"""convert to mcf/d"""
|
|
27
|
-
df = add_daily(df)
|
|
28
|
-
|
|
29
|
-
"""examine county-level histograms"""
|
|
30
|
-
|
|
31
|
-
fig = px.histogram(df[df.county == "Midland"], x="gas_daily")
|
|
32
|
-
fig.show()
|
|
33
|
-
|
|
34
|
-
"""calculate average per county"""
|
|
35
|
-
|
|
36
|
-
county_df = (
|
|
37
|
-
df[["state_ab", "county", "gas_daily"]]
|
|
38
|
-
.groupby(["state_ab", "county"], as_index=False)
|
|
39
|
-
.mean()
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
"""Get FIPS code Lookup Table"""
|
|
43
|
-
|
|
44
|
-
fips_df = get_fips()
|
|
45
|
-
|
|
46
|
-
"""merge fips_df with county_df"""
|
|
47
|
-
county_df = county_df.merge(
|
|
48
|
-
fips_df[fips_df.state_ab == "TX"], how="right", on=["state_ab", "county"]
|
|
49
|
-
)
|
|
50
|
-
county_df["gas_daily"] = county_df["gas_daily"].fillna(0)
|
|
51
|
-
|
|
52
|
-
"""Create county map"""
|
|
53
|
-
|
|
54
|
-
fig = ff.create_choropleth(
|
|
55
|
-
fips=county_df.fips.tolist(),
|
|
56
|
-
values=county_df.gas_daily.tolist(),
|
|
57
|
-
county_outline={"color": "rgb(255,255,255)", "width": 0.5},
|
|
58
|
-
scope=["TX"],
|
|
59
|
-
binning_endpoints=[75, 125, 250, 500, 1000, 2000, 4000, 8000, 16000, 32000],
|
|
60
|
-
)
|
|
61
|
-
fig.show()
|
|
File without changes
|
{synmax_api_python_client-4.0.0.dist-info → synmax_api_python_client-4.2.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|