polyapi-python 0.3.9.dev9__py3-none-any.whl → 0.3.9.dev10__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.
polyapi/auth.py CHANGED
@@ -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":
polyapi/deployables.py CHANGED
@@ -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."""
polyapi/generate.py CHANGED
@@ -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
 
polyapi/poly_tables.py ADDED
@@ -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)
polyapi/sync.py CHANGED
@@ -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}")
polyapi/typedefs.py CHANGED
@@ -11,7 +11,7 @@ class PropertySpecification(TypedDict):
11
11
 
12
12
 
13
13
  class PropertyType(TypedDict):
14
- kind: Literal['void', 'primitive', 'array', 'object', 'function', 'plain']
14
+ kind: Literal['void', 'primitive', 'array', 'object', 'function', 'plain', 'any']
15
15
  spec: NotRequired[Dict]
16
16
  name: NotRequired[str]
17
17
  type: NotRequired[str]
@@ -35,7 +35,7 @@ class SpecificationDto(TypedDict):
35
35
  description: str
36
36
  # function is none (or function key not present) if this is actually VariableSpecDto
37
37
  function: NotRequired[FunctionSpecification | None]
38
- type: Literal['apiFunction', 'customFunction', 'serverFunction', 'authFunction', 'webhookHandle', 'serverVariable']
38
+ type: Literal['apiFunction', 'customFunction', 'serverFunction', 'authFunction', 'webhookHandle', 'serverVariable', 'table']
39
39
  code: NotRequired[str]
40
40
  language: str
41
41
 
@@ -72,6 +72,17 @@ class SchemaSpecDto(TypedDict):
72
72
  # TODO add more
73
73
 
74
74
 
75
+ class TableSpecDto(TypedDict):
76
+ id: str
77
+ context: str
78
+ name: str
79
+ contextName: str
80
+ description: str
81
+ type: Literal['table']
82
+ schema: Dict[Any, Any]
83
+ unresolvedPolySchemaRefs: List
84
+
85
+
75
86
  Visibility = Union[Literal['PUBLIC'], Literal['TENANT'], Literal['ENVIRONMENT']]
76
87
 
77
88
 
@@ -91,3 +102,99 @@ class PolyServerFunction(PolyDeployable):
91
102
  class PolyClientFunction(PolyDeployable):
92
103
  logs_enabled: NotRequired[bool]
93
104
  visibility: NotRequired[Visibility]
105
+
106
+
107
+ class Table(TypedDict):
108
+ id: str
109
+ createdAt: str
110
+ updatedAt: str
111
+
112
+
113
+ class PolyCountResult(TypedDict):
114
+ count: int
115
+
116
+
117
+ class PolyDeleteResults(TypedDict):
118
+ deleted: int
119
+
120
+
121
+
122
+ QueryMode = Literal["default", "insensitive"]
123
+
124
+
125
+ SortOrder = Literal["asc", "desc"]
126
+
127
+ # Using functional form because of use of reserved keywords
128
+ StringFilter = TypedDict("StringFilter", {
129
+ "equals": NotRequired[str],
130
+ "in": NotRequired[List[str]],
131
+ "not_in": NotRequired[List[str]],
132
+ "lt": NotRequired[str],
133
+ "lte": NotRequired[str],
134
+ "gt": NotRequired[str],
135
+ "gte": NotRequired[str],
136
+ "contains": NotRequired[str],
137
+ "starts_with": NotRequired[str],
138
+ "ends_with": NotRequired[str],
139
+ "mode": NotRequired[QueryMode],
140
+ "not": NotRequired[Union[str, "StringFilter"]],
141
+ })
142
+
143
+ # Using functional form because of use of reserved keywords
144
+ NullableStringFilter = TypedDict("NullableStringFilter", {
145
+ "equals": NotRequired[Union[str, None]],
146
+ "in": NotRequired[List[str]],
147
+ "not_in": NotRequired[List[str]],
148
+ "lt": NotRequired[str],
149
+ "lte": NotRequired[str],
150
+ "gt": NotRequired[str],
151
+ "gte": NotRequired[str],
152
+ "contains": NotRequired[str],
153
+ "starts_with": NotRequired[str],
154
+ "ends_with": NotRequired[str],
155
+ "mode": NotRequired[QueryMode],
156
+ "not": NotRequired[Union[str, None, "NullableStringFilter"]],
157
+ })
158
+
159
+ # Using functional form because of use of reserved keywords
160
+ NumberFilter = TypedDict("NumberFilter", {
161
+ "equals": NotRequired[Union[int, float]],
162
+ "in": NotRequired[List[Union[int, float]]],
163
+ "not_in": NotRequired[List[Union[int, float]]],
164
+ "lt": NotRequired[Union[int, float]],
165
+ "lte": NotRequired[Union[int, float]],
166
+ "gt": NotRequired[Union[int, float]],
167
+ "gte": NotRequired[Union[int, float]],
168
+ "not": NotRequired[Union[int, float, "NumberFilter"]],
169
+ })
170
+
171
+ # Using functional form because of use of reserved keywords
172
+ NullableNumberFilter = TypedDict("NullableNumberFilter", {
173
+ "equals": NotRequired[Union[int, float, None]],
174
+ "in": NotRequired[List[Union[int, float]]],
175
+ "not_in": NotRequired[List[Union[int, float]]],
176
+ "lt": NotRequired[Union[int, float]],
177
+ "lte": NotRequired[Union[int, float]],
178
+ "gt": NotRequired[Union[int, float]],
179
+ "gte": NotRequired[Union[int, float]],
180
+ "not": NotRequired[Union[int, float, None, "NullableNumberFilter"]],
181
+ })
182
+
183
+
184
+ # Using functional form because of use of reserved keywords
185
+ BooleanFilter = TypedDict("BooleanFilter", {
186
+ "equals": NotRequired[bool],
187
+ "not": NotRequired[Union[bool, "BooleanFilter"]],
188
+ })
189
+
190
+ # Using functional form because of use of reserved keywords
191
+ NullableBooleanFilter = TypedDict("NullableBooleanFilter", {
192
+ "equals": NotRequired[Union[bool, None]],
193
+ "not": NotRequired[Union[bool, None, "NullableBooleanFilter"]],
194
+ })
195
+
196
+ # Using functional form because of use of reserved keywords
197
+ NullableObjectFilter = TypedDict("NullableObjectFilter", {
198
+ "equals": NotRequired[None],
199
+ "not": NotRequired[Union[None, "NullableObjectFilter"]],
200
+ })
polyapi/utils.py CHANGED
@@ -1,9 +1,8 @@
1
1
  import keyword
2
2
  import re
3
3
  import os
4
- import uuid
5
4
  from urllib.parse import urlparse
6
- from typing import Tuple, List
5
+ from typing import Tuple, List, Optional
7
6
  from colorama import Fore, Style
8
7
  from polyapi.constants import BASIC_PYTHON_TYPES
9
8
  from polyapi.typedefs import PropertySpecification, PropertyType
@@ -20,15 +19,17 @@ from polyapi.schema import (
20
19
  CODE_IMPORTS = "from typing import List, Dict, Any, Optional, Callable\nfrom typing_extensions import TypedDict, NotRequired\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url, get_direct_execute_config\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update, direct_execute\n\n"
21
20
 
22
21
 
23
- def init_the_init(full_path: str, code_imports="") -> None:
22
+ def init_the_init(full_path: str, code_imports: Optional[str] = None) -> None:
24
23
  init_path = os.path.join(full_path, "__init__.py")
25
24
  if not os.path.exists(init_path):
26
- code_imports = code_imports or CODE_IMPORTS
25
+ if code_imports is None:
26
+ code_imports = CODE_IMPORTS
27
27
  with open(init_path, "w") as f:
28
28
  f.write(code_imports)
29
29
 
30
30
 
31
- def add_import_to_init(full_path: str, next: str, code_imports="") -> None:
31
+ def add_import_to_init(full_path: str, next: str, code_imports: Optional[str] = None) -> None:
32
+ init_the_init(full_path, code_imports=code_imports)
32
33
  init_the_init(full_path, code_imports=code_imports)
33
34
 
34
35
  init_path = os.path.join(full_path, "__init__.py")
polyapi/variables.py CHANGED
@@ -19,10 +19,7 @@ GET_TEMPLATE = """
19
19
 
20
20
 
21
21
  TEMPLATE = """
22
- import uuid
23
-
24
-
25
- client_id = uuid.uuid4().hex
22
+ from polyapi.poly.client_id import client_id
26
23
 
27
24
 
28
25
  class {variable_name}:{get_method}
polyapi/webhook.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import socketio # type: ignore
3
3
  from socketio.exceptions import ConnectionError # type: ignore
4
- import uuid
5
4
  import logging
6
5
  from typing import Any, Dict, List, Tuple
7
6
 
@@ -33,6 +32,7 @@ async def {function_name}(
33
32
  Function ID: {function_id}
34
33
  \"""
35
34
  from polyapi.webhook import client, active_handlers
35
+ from polyapi.poly.client_id import client_id
36
36
 
37
37
  print("Starting webhook handler for {function_path}...")
38
38
 
@@ -40,7 +40,7 @@ async def {function_name}(
40
40
  raise Exception("Client not initialized. Abort!")
41
41
 
42
42
  options = options or {{}}
43
- eventsClientId = "{client_id}"
43
+ eventsClientId = client_id
44
44
  function_id = "{function_id}"
45
45
 
46
46
  api_key, base_url = get_api_key_and_url()
@@ -131,7 +131,6 @@ def render_webhook_handle(
131
131
 
132
132
  func_str = WEBHOOK_TEMPLATE.format(
133
133
  description=function_description,
134
- client_id=uuid.uuid4().hex,
135
134
  function_id=function_id,
136
135
  function_name=function_name,
137
136
  function_args=function_args,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.9.dev9
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,31 +1,32 @@
1
1
  polyapi/__init__.py,sha256=y-NBe7eopIdiqvJOD9KRjz90AgTR-zziBovF9h-CNzo,3725
2
2
  polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
3
3
  polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
4
- polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
4
+ polyapi/auth.py,sha256=EGstBjTSdAydI5hGAHeRRc1GcmHshogudb3sxCgO6zA,5341
5
5
  polyapi/cli.py,sha256=unKqAoZ1hTGAeyYRfNQ6jO15Um7N4F95k__1qFue5bI,10659
6
6
  polyapi/client.py,sha256=DW6ljG_xCwAo2yz23A9QfLooE6ZUDvSpdA4e_dCQjiQ,1418
7
7
  polyapi/config.py,sha256=cAMv2n9tGN_BTvqt7V32o5F86qRhxAKyey_PoId2D8s,7638
8
8
  polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
9
- polyapi/deployables.py,sha256=vaZvdkPpwgjVtRXD4IxFsJ90Qmgs-AOIB3ygy4QCG78,12100
9
+ polyapi/deployables.py,sha256=W8K6D8dypqntwjC9jAHxoR7hTfnG3Oj49nVULtq1OTQ,12133
10
10
  polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
11
11
  polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
12
12
  polyapi/execute.py,sha256=WQH5bLG3zLpb5_21y9dUvhCLJy9kiryWcTvr9glCji0,4404
13
13
  polyapi/function_cli.py,sha256=H0sVrbvRBXw_xeApe2MvQw8p_xE7jVTTOU-07Dg041A,4220
14
- polyapi/generate.py,sha256=PJvlF2W6MNHoQ3awVqW9UAwNn_MxasWslBgD2c2ZXM0,20642
14
+ polyapi/generate.py,sha256=thOrjq4suOkR97x00rthor96aslzzOaAAo4yxLtiuh0,21081
15
15
  polyapi/parser.py,sha256=20ZE7kSXx3UL7QVSIYYxzsnJlygVbsaDAg9q7c41WxQ,20695
16
16
  polyapi/poly_schemas.py,sha256=fZ6AGvHcOKQJtlrzSuzeBNed5DxPMA2dJGdJvuFCHWM,9066
17
+ polyapi/poly_tables.py,sha256=kK2hvyOpEOI1cf54vmVYUuEsBuH08UHGUpJoo96qDg0,15559
17
18
  polyapi/prepare.py,sha256=NQzpMIoakNovStvOGOmqSYIpTwiWXaweNSE9se10A2E,7420
18
19
  polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
20
21
  polyapi/schema.py,sha256=NyS9OgQJM0YAuZ8CLIM_9QitfpZf4lU5MFHlyFZKHCM,5498
21
22
  polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
22
- polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
23
- polyapi/typedefs.py,sha256=vJLZYBNmR3i8yQEDYlu1UfvtJyg6E1R1QyGlgFUm2rU,2362
24
- polyapi/utils.py,sha256=1F7Dwst_PbPuUBUSxx5r8d2DHDgqHtu07QW92T_YSdw,12454
25
- polyapi/variables.py,sha256=VAp2d5I-4WLYHCPF1w3pqU4-z8_XRQpYW-ddOw6G5S4,7268
26
- polyapi/webhook.py,sha256=gWYXHz0PnB_uY_lnHeUlg3EIHfTGwF-Tc6UaatldZBw,5333
27
- polyapi_python-0.3.9.dev9.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
28
- polyapi_python-0.3.9.dev9.dist-info/METADATA,sha256=X_kzW04KV0bqctEnglsfuLv6YBOTh9A1ovP27hAPeWk,5784
29
- polyapi_python-0.3.9.dev9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- polyapi_python-0.3.9.dev9.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
31
- polyapi_python-0.3.9.dev9.dist-info/RECORD,,
23
+ polyapi/sync.py,sha256=5iiQHu76my2AuGVoPRjD2t9Y0VvGJCZ8W2uG0F1sWPY,6722
24
+ polyapi/typedefs.py,sha256=VEaYODLm-3a26_cK1uSRoYwenmprLOQQdoKFz4gqK_0,5587
25
+ polyapi/utils.py,sha256=RpkXWi6jiwjozrX9iovPBK708w0W117ueN41uhQgnZU,12567
26
+ polyapi/variables.py,sha256=SJv106ePpQP5mx7Iiafl_shtFlE8FoaO9Q8lvw-3IRg,7270
27
+ polyapi/webhook.py,sha256=I3_uOl4f4L2-2WehzRLMVMRrB-76EiXCPA9Vzoaj30I,5326
28
+ polyapi_python-0.3.9.dev10.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
29
+ polyapi_python-0.3.9.dev10.dist-info/METADATA,sha256=99zj7bgpFyuE7CH8JlWQb8Ic0OayYpxwjOsP43vffSI,5785
30
+ polyapi_python-0.3.9.dev10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ polyapi_python-0.3.9.dev10.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
32
+ polyapi_python-0.3.9.dev10.dist-info/RECORD,,