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.
File without changes
@@ -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)
@@ -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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synmax-api-python-client
3
- Version: 4.0.0
3
+ Version: 4.2.1
4
4
  Summary: Synmax API client
5
5
  Home-page: https://github.com/SynMaxDev/synmax-api-python-client.git
6
6
  Author: Eric Anderson and Deepa Aswathaiah
@@ -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.0.0.dist-info/METADATA,sha256=3QD863d4OisG4eU47kl64hxVPNIivPS6XG9PaMrljHU,4860
26
- synmax_api_python_client-4.0.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
27
- synmax_api_python_client-4.0.0.dist-info/top_level.txt,sha256=SAjmDfHlJzUmjoGCT1SnS4qGSLN_ZF-p58UN1yW5kB0,17
28
- synmax_api_python_client-4.0.0.dist-info/RECORD,,
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()