polyapi-python 0.3.9.dev8__tar.gz → 0.3.9.dev10__tar.gz

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.
Files changed (49) hide show
  1. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/PKG-INFO +1 -1
  2. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/auth.py +3 -3
  3. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/deployables.py +8 -8
  4. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/generate.py +31 -16
  5. polyapi_python-0.3.9.dev10/polyapi/poly_tables.py +443 -0
  6. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/sync.py +9 -4
  7. polyapi_python-0.3.9.dev10/polyapi/typedefs.py +200 -0
  8. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/utils.py +6 -5
  9. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/variables.py +1 -4
  10. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/webhook.py +2 -3
  11. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi_python.egg-info/PKG-INFO +1 -1
  12. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi_python.egg-info/SOURCES.txt +2 -0
  13. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/pyproject.toml +1 -1
  14. polyapi_python-0.3.9.dev10/tests/test_tabi.py +619 -0
  15. polyapi_python-0.3.9.dev8/polyapi/typedefs.py +0 -93
  16. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/LICENSE +0 -0
  17. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/README.md +0 -0
  18. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/__init__.py +0 -0
  19. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/__main__.py +0 -0
  20. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/api.py +0 -0
  21. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/cli.py +0 -0
  22. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/client.py +0 -0
  23. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/config.py +0 -0
  24. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/constants.py +0 -0
  25. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/error_handler.py +0 -0
  26. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/exceptions.py +0 -0
  27. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/execute.py +0 -0
  28. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/function_cli.py +0 -0
  29. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/parser.py +0 -0
  30. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/poly_schemas.py +0 -0
  31. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/prepare.py +0 -0
  32. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/py.typed +0 -0
  33. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/rendered_spec.py +0 -0
  34. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/schema.py +0 -0
  35. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi/server.py +0 -0
  36. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi_python.egg-info/dependency_links.txt +0 -0
  37. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi_python.egg-info/requires.txt +0 -0
  38. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/polyapi_python.egg-info/top_level.txt +0 -0
  39. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/setup.cfg +0 -0
  40. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_api.py +0 -0
  41. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_auth.py +0 -0
  42. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_deployables.py +0 -0
  43. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_generate.py +0 -0
  44. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_parser.py +0 -0
  45. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_rendered_spec.py +0 -0
  46. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_schema.py +0 -0
  47. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_server.py +0 -0
  48. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_utils.py +0 -0
  49. {polyapi_python-0.3.9.dev8 → polyapi_python-0.3.9.dev10}/tests/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.9.dev8
3
+ Version: 0.3.9.dev10
4
4
  Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
5
5
  Author-email: Dan Fellin <dan@polyapi.io>
6
6
  License: MIT License
@@ -1,5 +1,4 @@
1
1
  from typing import List, Dict, Any, Tuple
2
- import uuid
3
2
 
4
3
  from polyapi.typedefs import PropertySpecification
5
4
  from polyapi.utils import parse_arguments, get_type_and_def
@@ -26,7 +25,8 @@ async def getToken(clientId: str, clientSecret: str, scopes: List[str], callback
26
25
 
27
26
  Function ID: {function_id}
28
27
  \"""
29
- eventsClientId = "{client_id}"
28
+ from polyapi.poly.client_id import client_id
29
+ eventsClientId = client_id
30
30
  function_id = "{function_id}"
31
31
 
32
32
  options = options or {{}}
@@ -165,7 +165,7 @@ def render_auth_function(
165
165
  func_str = ""
166
166
 
167
167
  if function_name == "getToken":
168
- func_str = GET_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description, client_id=uuid.uuid4().hex)
168
+ func_str = GET_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description)
169
169
  elif function_name == "introspectToken":
170
170
  func_str = INTROSPECT_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description)
171
171
  elif function_name == "refreshToken":
@@ -65,20 +65,20 @@ class SyncDeployment(TypedDict, total=False):
65
65
  context: str
66
66
  name: str
67
67
  description: str
68
- type: str
68
+ type: DeployableTypes
69
69
  fileRevision: str
70
70
  file: str
71
71
  types: DeployableFunctionTypes
72
- typeSchemas: Dict[str, any]
72
+ typeSchemas: Dict[str, Any]
73
73
  dependencies: List[str]
74
- config: Dict[str, any]
74
+ config: Dict[str, Any]
75
75
  instance: str
76
- id: Optional[str] = None
77
- deployed: Optional[str] = None
76
+ id: Optional[str]
77
+ deployed: Optional[str]
78
78
 
79
79
  DeployableTypeEntries: List[Tuple[DeployableTypeNames, DeployableTypes]] = [
80
- ("PolyServerFunction", "server-function"),
81
- ("PolyClientFunction", "client-function"),
80
+ ("PolyServerFunction", "server-function"), # type: ignore
81
+ ("PolyClientFunction", "client-function"), # type: ignore
82
82
  ]
83
83
 
84
84
  DeployableTypeToName: Dict[DeployableTypeNames, DeployableTypes] = {name: type for name, type in DeployableTypeEntries}
@@ -175,7 +175,7 @@ def get_git_revision(branch_or_tag: str = "HEAD") -> str:
175
175
  return check_output(["git", "rev-parse", "--short", branch_or_tag], text=True).strip()
176
176
  except CalledProcessError:
177
177
  # Return a random 7-character hash as a fallback
178
- return "".join(format(ord(c), 'x') for c in os.urandom(4))[:7]
178
+ return "".join(format(ord(str(c)), 'x') for c in os.urandom(4))[:7]
179
179
 
180
180
  def get_cache_deployments_revision() -> str:
181
181
  """Retrieve the cache deployments revision from a file."""
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import requests
3
3
  import os
4
+ import uuid
4
5
  import shutil
5
6
  import logging
6
7
  import tempfile
@@ -13,11 +14,12 @@ from .client import render_client_function
13
14
  from .poly_schemas import generate_schemas
14
15
  from .webhook import render_webhook_handle
15
16
 
16
- from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto
17
+ from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto, TableSpecDto
17
18
  from .api import render_api_function
18
19
  from .server import render_server_function
19
20
  from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
20
21
  from .variables import generate_variables
22
+ from .poly_tables import generate_tables
21
23
  from .config import get_api_key_and_url, get_direct_execute_config, get_cached_generate_args
22
24
 
23
25
  SUPPORTED_FUNCTION_TYPES = {
@@ -28,7 +30,7 @@ SUPPORTED_FUNCTION_TYPES = {
28
30
  "webhookHandle",
29
31
  }
30
32
 
31
- SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet"}
33
+ SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet", "table"}
32
34
 
33
35
 
34
36
  X_POLY_REF_WARNING = '''"""
@@ -195,16 +197,18 @@ def read_cached_specs() -> List[SpecificationDto]:
195
197
  return json.loads(f.read())
196
198
 
197
199
 
198
- def get_variables() -> List[VariableSpecDto]:
199
- specs = read_cached_specs()
200
+ def get_variables(specs: List[SpecificationDto]) -> List[VariableSpecDto]:
200
201
  return [cast(VariableSpecDto, spec) for spec in specs if spec["type"] == "serverVariable"]
201
202
 
202
203
 
203
- def get_schemas() -> List[SchemaSpecDto]:
204
- specs = read_cached_specs()
204
+ def get_schemas(specs: List[SpecificationDto]) -> List[SchemaSpecDto]:
205
205
  return [cast(SchemaSpecDto, spec) for spec in specs if spec["type"] == "schema"]
206
206
 
207
207
 
208
+ def get_tables(specs: List[SpecificationDto]) -> List[TableSpecDto]:
209
+ return [cast(TableSpecDto, spec) for spec in specs if spec["type"] == "table"]
210
+
211
+
208
212
  def remove_old_library():
209
213
  currdir = os.path.dirname(os.path.abspath(__file__))
210
214
  path = os.path.join(currdir, "poly")
@@ -219,6 +223,10 @@ def remove_old_library():
219
223
  if os.path.exists(path):
220
224
  shutil.rmtree(path)
221
225
 
226
+ path = os.path.join(currdir, "tabi")
227
+ if os.path.exists(path):
228
+ shutil.rmtree(path)
229
+
222
230
 
223
231
  def create_empty_schemas_module():
224
232
  """Create an empty schemas module for no-types mode so user code can still import from polyapi.schemas"""
@@ -277,6 +285,14 @@ sys.modules[__name__] = _SchemaModule()
277
285
  ''')
278
286
 
279
287
 
288
+ def _generate_client_id() -> None:
289
+ full_path = os.path.dirname(os.path.abspath(__file__))
290
+ full_path = os.path.join(full_path, "poly", "client_id.py")
291
+ with open(full_path, "w") as f:
292
+ f.write(f'client_id = "{uuid.uuid4().hex}"')
293
+
294
+
295
+
280
296
  def generate_from_cache() -> None:
281
297
  """
282
298
  Generate using cached values after non-explicit call.
@@ -333,9 +349,11 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
333
349
  limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
334
350
  functions = parse_function_specs(specs, limit_ids=limit_ids)
335
351
 
352
+ _generate_client_id()
353
+
336
354
  # Only process schemas if no_types is False
337
355
  if not no_types:
338
- schemas = get_schemas()
356
+ schemas = get_schemas(specs)
339
357
  schema_index = build_schema_index(schemas)
340
358
  if schemas:
341
359
  schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
@@ -359,7 +377,11 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
359
377
  )
360
378
  exit()
361
379
 
362
- variables = get_variables()
380
+ tables = get_tables(specs)
381
+ if tables:
382
+ generate_tables(tables)
383
+
384
+ variables = get_variables(specs)
363
385
  if variables:
364
386
  generate_variables(variables)
365
387
 
@@ -371,14 +393,7 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
371
393
 
372
394
 
373
395
  def clear() -> None:
374
- base = os.path.dirname(os.path.abspath(__file__))
375
- poly_path = os.path.join(base, "poly")
376
- if os.path.exists(poly_path):
377
- shutil.rmtree(poly_path)
378
-
379
- vari_path = os.path.join(base, "vari")
380
- if os.path.exists(vari_path):
381
- shutil.rmtree(vari_path)
396
+ remove_old_library()
382
397
  print("Cleared!")
383
398
 
384
399
 
@@ -0,0 +1,443 @@
1
+ import os
2
+ import requests
3
+ from typing_extensions import NotRequired, TypedDict
4
+ from typing import List, Union, Type, Dict, Any, Literal, Tuple, Optional, get_args, get_origin
5
+ from polyapi.utils import add_import_to_init, init_the_init
6
+ from polyapi.typedefs import TableSpecDto
7
+ from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
8
+
9
+
10
+ def execute_query(table_id, method, query):
11
+ from polyapi import polyCustom
12
+ from polyapi.poly.client_id import client_id
13
+ try:
14
+ url = f"/tables/{table_id}/{method}?clientId={client_id}"
15
+ headers = {{
16
+ 'x-poly-execution-id': polyCustom.get('executionId')
17
+ }}
18
+ response = requests.post(url, json=query, headers=headers)
19
+ response.raise_for_status()
20
+ return response.json()
21
+ except Exception as e:
22
+ return scrub_keys(e)
23
+
24
+
25
+ def first_result(rsp):
26
+ if isinstance(rsp, dict) and isinstance(rsp.get('results'), list):
27
+ return rsp['results'][0] if rsp['results'] else None
28
+ return rsp
29
+
30
+
31
+ _key_transform_map = {
32
+ "not_": "not",
33
+ "in": "in",
34
+ "starts_with": "startsWith",
35
+ "ends_with": "startsWith",
36
+ "not_in": "notIn",
37
+ }
38
+
39
+
40
+ def _transform_keys(obj: Any) -> Any:
41
+ if isinstance(obj, dict):
42
+ return {
43
+ _key_transform_map.get(k, k): _transform_keys(v)
44
+ for k, v in obj.items()
45
+ }
46
+
47
+ elif isinstance(obj, list):
48
+ return [_transform_keys(v) for v in obj]
49
+
50
+ else:
51
+ return obj
52
+
53
+
54
+ def transform_query(query: dict) -> dict:
55
+ if query["where"] or query["order_by"]:
56
+ return {
57
+ **query,
58
+ "where": _transform_keys(query["where"]) if query["where"] else None,
59
+ "orderBy": query["order_by"] if query["order_by"] else None
60
+ }
61
+
62
+ return query
63
+
64
+
65
+ TABI_TABLE_TEMPLATE = '''
66
+ {table_name}Columns = Literal[{table_columns}]
67
+
68
+
69
+
70
+ {table_row_classes}
71
+
72
+
73
+
74
+ {table_row_subset_class}
75
+
76
+
77
+
78
+ {table_where_class}
79
+
80
+
81
+
82
+ class {table_name}SelectManyQuery(TypedDict):
83
+ where: NotRequired[{table_name}WhereFilter]
84
+ order_by: NotRequired[Dict[{table_name}Columns, SortOrder]]
85
+ limit: NotRequired[int]
86
+ offset: NotRequired[int]
87
+
88
+
89
+
90
+ class {table_name}SelectOneQuery(TypedDict):
91
+ where: NotRequired[{table_name}WhereFilter]
92
+ order_by: NotRequired[Dict[{table_name}Columns, SortOrder]]
93
+
94
+
95
+
96
+ class {table_name}InsertOneQuery(TypedDict):
97
+ data: {table_name}Subset
98
+
99
+
100
+
101
+ class {table_name}InsertManyQuery(TypedDict):
102
+ data: List[{table_name}Subset]
103
+
104
+
105
+
106
+ class {table_name}UpdateManyQuery(TypedDict):
107
+ where: NotRequired[{table_name}WhereFilter]
108
+ data: {table_name}Subset
109
+
110
+
111
+
112
+ class {table_name}DeleteQuery(TypedDict):
113
+ where: NotRequired[{table_name}WhereFilter]
114
+
115
+
116
+
117
+ class {table_name}QueryResults(TypedDict):
118
+ results: List[{table_name}Row]
119
+ pagination: None # Pagination not yet supported
120
+
121
+
122
+
123
+ class {table_name}CountQuery(TypedDict):
124
+ where: NotRequired[{table_name}WhereFilter]
125
+
126
+
127
+
128
+ class {table_name}:{table_description}
129
+ table_id = "{table_id}"
130
+
131
+ @overload
132
+ @staticmethod
133
+ def count(query: {table_name}CountQuery) -> PolyCountResult: ...
134
+ @overload
135
+ @staticmethod
136
+ def count(*, where: Optional[{table_name}WhereFilter]) -> PolyCountResult: ...
137
+
138
+ @staticmethod
139
+ def count(*args, **kwargs) -> PolyCountResult:
140
+ if args:
141
+ if len(args) != 1 or not isinstance(args[0], dict):
142
+ raise TypeError("Expected query as a single argument or as kwargs")
143
+ query = args[0]
144
+ else:
145
+ query = kwargs
146
+ return execute_query({table_name}.table_id, "count", transform_query(query))
147
+
148
+ @overload
149
+ @staticmethod
150
+ def select_many(query: {table_name}SelectManyQuery) -> {table_name}QueryResults: ...
151
+ @overload
152
+ @staticmethod
153
+ def select_many(*, where: Optional[{table_name}WhereFilter], order_by: Optional[Dict[{table_name}Columns, SortOrder]], limit: Optional[int], offset: Optional[int]) -> {table_name}QueryResults: ...
154
+
155
+ @staticmethod
156
+ def select_many(*args, **kwargs) -> {table_name}QueryResults:
157
+ if args:
158
+ if len(args) != 1 or not isinstance(args[0], dict):
159
+ raise TypeError("Expected query as a single argument or as kwargs")
160
+ query = args[0]
161
+ else:
162
+ query = kwargs
163
+ if query.get('limit') is None:
164
+ query['limit'] = 1000
165
+ if query['limit'] > 1000:
166
+ raise ValueError("Cannot select more than 1000 rows at a time.")
167
+ return execute_query({table_name}.table_id, "select", transform_query(query))
168
+
169
+ @overload
170
+ @staticmethod
171
+ def select_one(query: {table_name}SelectOneQuery) -> {table_name}Row: ...
172
+ @overload
173
+ @staticmethod
174
+ def select_one(*, where: Optional[{table_name}WhereFilter], order_by: Optional[Dict[{table_name}Columns, SortOrder]]) -> {table_name}Row: ...
175
+
176
+ @staticmethod
177
+ def select_one(*args, **kwargs) -> {table_name}Row:
178
+ if args:
179
+ if len(args) != 1 or not isinstance(args[0], dict):
180
+ raise TypeError("Expected query as a single argument or as kwargs")
181
+ query = args[0]
182
+ else:
183
+ query = kwargs
184
+ query['limit'] = 1
185
+ return first_result(execute_query({table_name}.table_id, "select", transform_query(query)))
186
+
187
+ @overload
188
+ @staticmethod
189
+ def insert_many(query: {table_name}InsertManyQuery) -> {table_name}QueryResults: ...
190
+ @overload
191
+ @staticmethod
192
+ def insert_many(*, data: List[{table_name}Subset]) -> {table_name}QueryResults: ...
193
+
194
+ @staticmethod
195
+ def insert_many(*args, **kwargs) -> {table_name}QueryResults:
196
+ if args:
197
+ if len(args) != 1 or not isinstance(args[0], dict):
198
+ raise TypeError("Expected query as a single argument or as kwargs")
199
+ query = args[0]
200
+ else:
201
+ query = kwargs
202
+ if len(query['data']) > 1000:
203
+ raise ValueError("Cannot insert more than 1000 rows at a time.")
204
+ return execute_query({table_name}.table_id, "insert", query)
205
+
206
+ @overload
207
+ @staticmethod
208
+ def insert_one(query: {table_name}InsertOneQuery) -> {table_name}Row: ...
209
+ @overload
210
+ @staticmethod
211
+ def insert_one(*, data: {table_name}Subset) -> {table_name}Row: ...
212
+
213
+ @staticmethod
214
+ def insert_one(*args, **kwargs) -> {table_name}Row:
215
+ if args:
216
+ if len(args) != 1 or not isinstance(args[0], dict):
217
+ raise TypeError("Expected query as a single argument or as kwargs")
218
+ query = args[0]
219
+ else:
220
+ query = kwargs
221
+ return first_result(execute_query({table_name}.table_id, "insert", {{ 'data': [query['data']] }}))
222
+
223
+ @overload
224
+ @staticmethod
225
+ def upsert_many(query: {table_name}InsertManyQuery) -> {table_name}QueryResults: ...
226
+ @overload
227
+ @staticmethod
228
+ def upsert_many(*, data: List[{table_name}Subset]) -> {table_name}QueryResults: ...
229
+
230
+ @staticmethod
231
+ def upsert_many(*args, **kwargs) -> {table_name}QueryResults:
232
+ if args:
233
+ if len(args) != 1 or not isinstance(args[0], dict):
234
+ raise TypeError("Expected query as a single argument or as kwargs")
235
+ query = args[0]
236
+ else:
237
+ query = kwargs
238
+ if len(data) > 1000:
239
+ raise ValueError("Cannot upsert more than 1000 rows at a time.")
240
+ return execute_query({table_name}.table_id, "upsert", query)
241
+
242
+ @overload
243
+ @staticmethod
244
+ def upsert_one(query: {table_name}InsertOneQuery) -> {table_name}Row: ...
245
+ @overload
246
+ @staticmethod
247
+ def upsert_one(*, data: {table_name}Subset) -> {table_name}Row: ...
248
+
249
+ @staticmethod
250
+ def upsert_one(*args, **kwargs) -> {table_name}Row:
251
+ if args:
252
+ if len(args) != 1 or not isinstance(args[0], dict):
253
+ raise TypeError("Expected query as a single argument or as kwargs")
254
+ query = args[0]
255
+ else:
256
+ query = kwargs
257
+ return first_result(execute_query({table_name}.table_id, "upsert", {{ 'data': [query['data']] }}))
258
+
259
+ @overload
260
+ @staticmethod
261
+ def update_many(query: {table_name}UpdateManyQuery) -> {table_name}QueryResults: ...
262
+ @overload
263
+ @staticmethod
264
+ def update_many(*, where: Optional[{table_name}WhereFilter], data: {table_name}Subset) -> {table_name}QueryResults: ...
265
+
266
+ @staticmethod
267
+ def update_many(*args, **kwargs) -> {table_name}QueryResults:
268
+ if args:
269
+ if len(args) != 1 or not isinstance(args[0], dict):
270
+ raise TypeError("Expected query as a single argument or as kwargs")
271
+ query = args[0]
272
+ else:
273
+ query = kwargs
274
+ return execute_query({table_name}.table_id, "update", transform_query(query))
275
+
276
+ @overload
277
+ @staticmethod
278
+ def delete_many(query: {table_name}DeleteQuery) -> PolyDeleteResults: ...
279
+ @overload
280
+ @staticmethod
281
+ def delete_many(*, where: Optional[{table_name}WhereFilter]) -> PolyDeleteResults: ...
282
+
283
+ @staticmethod
284
+ def delete_many(*args, **kwargs) -> PolyDeleteResults:
285
+ if args:
286
+ if len(args) != 1 or not isinstance(args[0], dict):
287
+ raise TypeError("Expected query as a single argument or as kwargs")
288
+ query = args[0]
289
+ else:
290
+ query = kwargs
291
+ return execute_query({table_name}.table_id, "delete", query)
292
+ '''
293
+
294
+
295
+ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -> str:
296
+ result = ""
297
+
298
+ col_type = schema.get("type", "object")
299
+ if isinstance(col_type, list):
300
+ subtypes = [_get_column_type_str(name, { **schema, "type": t }, is_required) for t in col_type]
301
+ result = f"Union[{", ".join(subtypes)}]"
302
+ elif col_type == "array":
303
+ if isinstance(schema["items"], list):
304
+ subtypes = [_get_column_type_str(f"{name}{i}", s, True) for i, s in enumerate(schema["items"])]
305
+ result = f"Tuple[{", ".join(subtypes)}]"
306
+ elif isinstance(schema["items"], dict):
307
+ result = f"List[{_get_column_type_str(name, schema["items"], True)}]"
308
+ else:
309
+ result = "List[Any]"
310
+ elif col_type == "object":
311
+ if isinstance(schema.get("patternProperties"), dict):
312
+ # TODO: Handle multiple pattern properties
313
+ result = f"Dict[str, {_get_column_type_str(f"{name}_", schema["patternProperties"], True)}]"
314
+ elif isinstance(schema.get("properties"), dict) and len(schema["properties"].values()) > 0:
315
+ # TODO: Handle x-poly-refs
316
+ result = f'"{name}"'
317
+ else:
318
+ result = "Dict[str, Any]"
319
+ else:
320
+ result = JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(schema["type"], "")
321
+
322
+ if result:
323
+ return result if is_required else f"Optional[{result}]"
324
+
325
+ return "Any"
326
+
327
+
328
+ def _render_table_row_classes(table_name: str, schema: Dict[str, Any]) -> str:
329
+ from polyapi.schema import wrapped_generate_schema_types
330
+
331
+ output = wrapped_generate_schema_types(schema, f"{table_name}Row", "Dict")
332
+
333
+ return output[1].split("\n", 1)[1].strip()
334
+
335
+
336
+ def _render_table_subset_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
337
+ # Generate class which can match any subset of a table row
338
+ lines = [f"class {table_name}Subset(TypedDict):"]
339
+
340
+ for name, schema in columns:
341
+ type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, name in required)
342
+ lines.append(f" {name}: NotRequired[{type_str}]")
343
+
344
+ return "\n".join(lines)
345
+
346
+
347
+ def _render_table_where_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
348
+ # Generate class for the 'where' part of the query
349
+ lines = [f"class {table_name}WhereFilter(TypedDict):"]
350
+
351
+ for name, schema in columns:
352
+ ftype_str = ""
353
+ type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, True) # force required to avoid wrapping type in Optional[]
354
+ is_required = name in required
355
+ if type_str == "bool":
356
+ ftype_str = "BooleanFilter" if is_required else "NullableBooleanFilter"
357
+ elif type_str == "str":
358
+ ftype_str = "StringFilter" if is_required else "NullableStringFilter"
359
+ elif type_str in ["int", "float"]:
360
+ ftype_str = "NumberFilter" if is_required else "NullableNumberFilter"
361
+ elif is_required == False:
362
+ type_str = "None"
363
+ ftype_str = "NullableObjectFilter"
364
+
365
+ if ftype_str:
366
+ lines.append(f" {name}: NotRequired[Union[{type_str}, {ftype_str}]]")
367
+
368
+ lines.append(f' AND: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
369
+ lines.append(f' OR: NotRequired[List["{table_name}WhereFilter"]]')
370
+ lines.append(f' NOT: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
371
+
372
+ return "\n".join(lines)
373
+
374
+
375
+ def _render_table(table: TableSpecDto) -> str:
376
+ columns = list(table["schema"]["properties"].items())
377
+ required_colunms = table["schema"].get("required", [])
378
+
379
+ table_columns = ",".join([ f'"{k}"' for k,_ in columns])
380
+ table_row_classes = _render_table_row_classes(table["name"], table["schema"])
381
+ table_row_subset_class = _render_table_subset_class(table["name"], columns, required_colunms)
382
+ table_where_class = _render_table_where_class(table["name"], columns, required_colunms)
383
+ if table.get("description", ""):
384
+ table_description = '\n """'
385
+ table_description += '\n '.join(table["description"].replace('"', "'").split("\n"))
386
+ table_description += '\n """'
387
+ else:
388
+ table_description = ""
389
+
390
+ return TABI_TABLE_TEMPLATE.format(
391
+ table_name=table["name"],
392
+ table_id=table["id"],
393
+ table_description=table_description,
394
+ table_columns=table_columns,
395
+ table_row_classes=table_row_classes,
396
+ table_row_subset_class=table_row_subset_class,
397
+ table_where_class=table_where_class,
398
+ )
399
+
400
+
401
+ def generate_tables(tables: List[TableSpecDto]):
402
+ for table in tables:
403
+ _create_table(table)
404
+
405
+
406
+ def _create_table(table: TableSpecDto) -> None:
407
+ folders = ["tabi"]
408
+ if table["context"]:
409
+ folders += table["context"].split(".")
410
+
411
+ # build up the full_path by adding all the folders
412
+ base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)))
413
+ full_path = base_path
414
+
415
+ for idx, folder in enumerate(folders):
416
+ full_path = os.path.join(full_path, folder)
417
+ if not os.path.exists(full_path):
418
+ os.makedirs(full_path)
419
+ next = folders[idx + 1] if idx + 1 < len(folders) else None
420
+ if next:
421
+ add_import_to_init(full_path, next, "")
422
+
423
+ init_path = os.path.join(full_path, "__init__.py")
424
+
425
+ imports = "\n".join([
426
+ "from typing_extensions import NotRequired, TypedDict",
427
+ "from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
428
+ "from polyapi.poly_tables import execute_query, first_result, transform_query",
429
+ "from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
430
+ ])
431
+ table_contents = _render_table(table)
432
+
433
+ file_contents = ""
434
+ if os.path.exists(init_path):
435
+ with open(init_path, "r") as f:
436
+ file_contents = f.read()
437
+
438
+ with open(init_path, "w") as f:
439
+ if not file_contents.startswith(imports):
440
+ f.write(imports + "\n\n\n")
441
+ if file_contents:
442
+ f.write(file_contents + "\n\n\n")
443
+ f.write(table_contents)
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from datetime import datetime
3
3
  from typing import List, Dict
4
+ from typing_extensions import cast # type: ignore
4
5
  import requests
5
6
 
6
7
  from polyapi.utils import get_auth_headers
@@ -30,12 +31,14 @@ def group_by(items: List[Dict], key: str) -> Dict[str, List[Dict]]:
30
31
 
31
32
  def remove_deployable_function(deployable: SyncDeployment) -> bool:
32
33
  api_key, _ = get_api_key_and_url()
34
+ if not api_key:
35
+ raise Error("Missing api key!")
33
36
  headers = get_auth_headers(api_key)
34
37
  url = f'{deployable["instance"]}/functions/{deployable["type"].replace("-function", "")}/{deployable["id"]}'
35
38
  response = requests.get(url, headers=headers)
36
39
  if response.status_code != 200:
37
40
  return False
38
- requests.delete(url, headers)
41
+ requests.delete(url, headers=headers)
39
42
  return True
40
43
 
41
44
  def remove_deployable(deployable: SyncDeployment) -> bool:
@@ -47,6 +50,8 @@ def remove_deployable(deployable: SyncDeployment) -> bool:
47
50
 
48
51
  def sync_function_and_get_id(deployable: SyncDeployment, code: str) -> str:
49
52
  api_key, _ = get_api_key_and_url()
53
+ if not api_key:
54
+ raise Error("Missing api key!")
50
55
  headers = get_auth_headers(api_key)
51
56
  url = f'{deployable["instance"]}/functions/{deployable["type"].replace("-function", "")}'
52
57
  payload = {
@@ -129,15 +134,15 @@ def sync_deployables(dry_run: bool, instance: str | None = None):
129
134
  else:
130
135
  sync_deployment = { **deployable, "instance": instance }
131
136
  if git_revision == deployable['gitRevision']:
132
- deployment = sync_deployable(sync_deployment)
137
+ deployment = sync_deployable(cast(SyncDeployment, sync_deployment))
133
138
  if previous_deployment:
134
139
  previous_deployment.update(deployment)
135
140
  else:
136
141
  deployable['deployments'].insert(0, deployment)
137
142
  else:
138
- found = remove_deployable(sync_deployment)
143
+ found = remove_deployable(cast(SyncDeployment, sync_deployment))
139
144
  action = 'NOT FOUND' if not found else action
140
- remove_index = all_deployables.index(deployable)
145
+ remove_index = all_deployables.index(cast(DeployableRecord, deployable))
141
146
  to_remove.append(all_deployables.pop(remove_index))
142
147
 
143
148
  print(f"{'Would sync' if dry_run else 'Synced'} {deployable['type'].replace('-', ' ')} {deployable['context']}.{deployable['name']}: {'TO BE ' if dry_run else ''}{action}")