polyapi-python 0.3.8.dev10__py3-none-any.whl → 0.3.9__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
@@ -1,4 +1,6 @@
1
1
  import os
2
+ import string
3
+ import random
2
4
  import subprocess
3
5
  import json
4
6
  import hashlib
@@ -65,20 +67,21 @@ class SyncDeployment(TypedDict, total=False):
65
67
  context: str
66
68
  name: str
67
69
  description: str
68
- type: str
70
+ type: DeployableTypes
69
71
  fileRevision: str
70
72
  file: str
71
73
  types: DeployableFunctionTypes
72
- typeSchemas: Dict[str, any]
74
+ typeSchemas: Dict[str, Any]
73
75
  dependencies: List[str]
74
- config: Dict[str, any]
76
+ config: Dict[str, Any]
75
77
  instance: str
76
- id: Optional[str] = None
77
- deployed: Optional[str] = None
78
+ id: Optional[str]
79
+ deployed: Optional[str]
80
+
78
81
 
79
82
  DeployableTypeEntries: List[Tuple[DeployableTypeNames, DeployableTypes]] = [
80
- ("PolyServerFunction", "server-function"),
81
- ("PolyClientFunction", "client-function"),
83
+ ("PolyServerFunction", "server-function"), # type: ignore
84
+ ("PolyClientFunction", "client-function"), # type: ignore
82
85
  ]
83
86
 
84
87
  DeployableTypeToName: Dict[DeployableTypeNames, DeployableTypes] = {name: type for name, type in DeployableTypeEntries}
@@ -118,13 +121,13 @@ def get_all_deployable_files_windows(config: PolyDeployConfig) -> List[str]:
118
121
  pattern = ' '.join(f"/C:\"polyConfig: {name}\"" for name in config["type_names"]) or '/C:"polyConfig"'
119
122
 
120
123
  exclude_command = f" | findstr /V /I \"{exclude_pattern}\"" if exclude_pattern else ''
121
- search_command = f" | findstr /S /M /I /F:/ {pattern} *.*"
124
+ search_command = f" | findstr /M /I /F:/ {pattern}"
122
125
 
123
126
  result = []
124
127
  for dir_path in config["include_dirs"]:
125
128
  if dir_path != '.':
126
129
  include_pattern = " ".join(f"{dir_path}*.{f}" for f in config["include_files_or_extensions"]) or "*"
127
- dir_command = f"dir {include_pattern} /S /P /B > NUL"
130
+ dir_command = f"dir {include_pattern} /S /P /B"
128
131
  full_command = f"{dir_command}{exclude_command}{search_command}"
129
132
  try:
130
133
  output = subprocess.check_output(full_command, shell=True, text=True)
@@ -175,7 +178,7 @@ def get_git_revision(branch_or_tag: str = "HEAD") -> str:
175
178
  return check_output(["git", "rev-parse", "--short", branch_or_tag], text=True).strip()
176
179
  except CalledProcessError:
177
180
  # Return a random 7-character hash as a fallback
178
- return "".join(format(ord(c), 'x') for c in os.urandom(4))[:7]
181
+ return "".join([random.choice(string.ascii_letters + string.digits) for _ in range(7)])
179
182
 
180
183
  def get_cache_deployments_revision() -> str:
181
184
  """Retrieve the cache deployments revision from a file."""
polyapi/execute.py CHANGED
@@ -1,9 +1,13 @@
1
1
  from typing import Dict, Optional
2
2
  import requests
3
+ import os
4
+ import logging
3
5
  from requests import Response
4
6
  from polyapi.config import get_api_key_and_url, get_mtls_config
5
7
  from polyapi.exceptions import PolyApiException
6
8
 
9
+ logger = logging.getLogger("poly")
10
+
7
11
  def direct_execute(function_type, function_id, data) -> Response:
8
12
  """ execute a specific function id/type
9
13
  """
@@ -13,7 +17,11 @@ def direct_execute(function_type, function_id, data) -> Response:
13
17
 
14
18
  endpoint_info = requests.post(url, json=data, headers=headers)
15
19
  if endpoint_info.status_code < 200 or endpoint_info.status_code >= 300:
16
- raise PolyApiException(f"{endpoint_info.status_code}: {endpoint_info.content.decode('utf-8', errors='ignore')}")
20
+ error_content = endpoint_info.content.decode("utf-8", errors="ignore")
21
+ if function_type == 'api' and os.getenv("LOGS_ENABLED"):
22
+ raise PolyApiException(f"Error executing api function with id: {function_id}. Status code: {endpoint_info.status_code}. Request data: {data}, Response: {error_content}")
23
+ elif function_type != 'api':
24
+ raise PolyApiException(f"{endpoint_info.status_code}: {error_content}")
17
25
 
18
26
  endpoint_info_data = endpoint_info.json()
19
27
  request_params = endpoint_info_data.copy()
@@ -38,9 +46,12 @@ def direct_execute(function_type, function_id, data) -> Response:
38
46
  **request_params
39
47
  )
40
48
 
41
- if resp.status_code < 200 or resp.status_code >= 300:
49
+ if (resp.status_code < 200 or resp.status_code >= 300):
42
50
  error_content = resp.content.decode("utf-8", errors="ignore")
43
- raise PolyApiException(f"{resp.status_code}: {error_content}")
51
+ if function_type == 'api' and os.getenv("LOGS_ENABLED"):
52
+ logger.error(f"Error executing api function with id: {function_id}. Status code: {resp.status_code}. Request data: {data}, Response: {error_content}")
53
+ elif function_type != 'api':
54
+ raise PolyApiException(f"{resp.status_code}: {error_content}")
44
55
 
45
56
  return resp
46
57
 
@@ -59,9 +70,12 @@ def execute(function_type, function_id, data) -> Response:
59
70
  headers=headers,
60
71
  )
61
72
 
62
- if resp.status_code < 200 or resp.status_code >= 300:
73
+ if (resp.status_code < 200 or resp.status_code >= 300) and os.getenv("LOGS_ENABLED"):
63
74
  error_content = resp.content.decode("utf-8", errors="ignore")
64
- raise PolyApiException(f"{resp.status_code}: {error_content}")
75
+ if function_type == 'api' and os.getenv("LOGS_ENABLED"):
76
+ logger.error(f"Error executing api function with id: {function_id}. Status code: {resp.status_code}. Request data: {data}, Response: {error_content}")
77
+ elif function_type != 'api':
78
+ raise PolyApiException(f"{resp.status_code}: {error_content}")
65
79
 
66
80
  return resp
67
81
 
polyapi/generate.py CHANGED
@@ -1,9 +1,12 @@
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
8
+
9
+ from copy import deepcopy
7
10
  from typing import Any, List, Optional, Tuple, cast
8
11
 
9
12
  from .auth import render_auth_function
@@ -11,11 +14,12 @@ from .client import render_client_function
11
14
  from .poly_schemas import generate_schemas
12
15
  from .webhook import render_webhook_handle
13
16
 
14
- from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto
17
+ from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto, TableSpecDto
15
18
  from .api import render_api_function
16
19
  from .server import render_server_function
17
20
  from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
18
21
  from .variables import generate_variables
22
+ from .poly_tables import generate_tables
19
23
  from .config import get_api_key_and_url, get_direct_execute_config, get_cached_generate_args
20
24
 
21
25
  SUPPORTED_FUNCTION_TYPES = {
@@ -26,7 +30,7 @@ SUPPORTED_FUNCTION_TYPES = {
26
30
  "webhookHandle",
27
31
  }
28
32
 
29
- SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet"}
33
+ SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet", "table"}
30
34
 
31
35
 
32
36
  X_POLY_REF_WARNING = '''"""
@@ -136,19 +140,22 @@ def parse_function_specs(
136
140
  limit_ids: List[str] | None = None, # optional list of ids to limit to
137
141
  ) -> List[SpecificationDto]:
138
142
  functions = []
139
- for spec in specs:
140
- if not spec:
143
+ for raw_spec in specs:
144
+ if not raw_spec:
141
145
  continue
142
146
 
143
147
  # For no_types mode, we might not have function data, but we still want to include the spec
144
148
  # if it's a supported function type
145
- if spec["type"] not in SUPPORTED_FUNCTION_TYPES:
149
+ if raw_spec["type"] not in SUPPORTED_FUNCTION_TYPES:
146
150
  continue
147
151
 
148
152
  # Skip if we have a limit and this spec is not in it
149
- if limit_ids and spec.get("id") not in limit_ids:
153
+ if limit_ids and raw_spec.get("id") not in limit_ids:
150
154
  continue
151
155
 
156
+ # Should really be fixed in specs api, but for now handle json strings in arg schemas
157
+ spec = normalize_args_schema(raw_spec)
158
+
152
159
  # For customFunction, check language if we have function data
153
160
  if spec["type"] == "customFunction":
154
161
  if spec.get("language") and spec["language"] != "python":
@@ -190,16 +197,18 @@ def read_cached_specs() -> List[SpecificationDto]:
190
197
  return json.loads(f.read())
191
198
 
192
199
 
193
- def get_variables() -> List[VariableSpecDto]:
194
- specs = read_cached_specs()
200
+ def get_variables(specs: List[SpecificationDto]) -> List[VariableSpecDto]:
195
201
  return [cast(VariableSpecDto, spec) for spec in specs if spec["type"] == "serverVariable"]
196
202
 
197
203
 
198
- def get_schemas() -> List[SchemaSpecDto]:
199
- specs = read_cached_specs()
204
+ def get_schemas(specs: List[SpecificationDto]) -> List[SchemaSpecDto]:
200
205
  return [cast(SchemaSpecDto, spec) for spec in specs if spec["type"] == "schema"]
201
206
 
202
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
+
203
212
  def remove_old_library():
204
213
  currdir = os.path.dirname(os.path.abspath(__file__))
205
214
  path = os.path.join(currdir, "poly")
@@ -214,6 +223,10 @@ def remove_old_library():
214
223
  if os.path.exists(path):
215
224
  shutil.rmtree(path)
216
225
 
226
+ path = os.path.join(currdir, "tabi")
227
+ if os.path.exists(path):
228
+ shutil.rmtree(path)
229
+
217
230
 
218
231
  def create_empty_schemas_module():
219
232
  """Create an empty schemas module for no-types mode so user code can still import from polyapi.schemas"""
@@ -272,6 +285,14 @@ sys.modules[__name__] = _SchemaModule()
272
285
  ''')
273
286
 
274
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
+
275
296
  def generate_from_cache() -> None:
276
297
  """
277
298
  Generate using cached values after non-explicit call.
@@ -286,6 +307,37 @@ def generate_from_cache() -> None:
286
307
  )
287
308
 
288
309
 
310
+ def _parse_arg_schema(value: Any) -> Any:
311
+ if isinstance(value, str):
312
+ text = value.strip()
313
+ if text and text[0] in "{[":
314
+ try:
315
+ return json.loads(text)
316
+ except json.JSONDecodeError:
317
+ logging.warning("Could not parse function argument schema (leaving as str): %s", text[:200])
318
+ return value
319
+
320
+
321
+ def normalize_args_schema(
322
+ raw_spec: SpecificationDto
323
+ ) -> SpecificationDto:
324
+ spec = deepcopy(raw_spec)
325
+
326
+ function_block = spec.get("function")
327
+ if not isinstance(function_block, dict):
328
+ return spec
329
+ arguments_block = function_block.get("arguments")
330
+ if not isinstance(arguments_block, list):
331
+ return spec
332
+
333
+ for argument in arguments_block:
334
+ arg_type = argument.get("type")
335
+ if isinstance(arg_type, dict) and "schema" in arg_type:
336
+ arg_type["schema"] = _parse_arg_schema(arg_type["schema"])
337
+
338
+ return spec
339
+
340
+
289
341
  def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] = None, function_ids: Optional[List[str]] = None, no_types: bool = False) -> None:
290
342
  generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
291
343
  print(generate_msg, end="", flush=True)
@@ -297,9 +349,11 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
297
349
  limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
298
350
  functions = parse_function_specs(specs, limit_ids=limit_ids)
299
351
 
352
+ _generate_client_id()
353
+
300
354
  # Only process schemas if no_types is False
301
355
  if not no_types:
302
- schemas = get_schemas()
356
+ schemas = get_schemas(specs)
303
357
  schema_index = build_schema_index(schemas)
304
358
  if schemas:
305
359
  schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
@@ -323,7 +377,11 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
323
377
  )
324
378
  exit()
325
379
 
326
- variables = get_variables()
380
+ tables = get_tables(specs)
381
+ if tables:
382
+ generate_tables(tables)
383
+
384
+ variables = get_variables(specs)
327
385
  if variables:
328
386
  generate_variables(variables)
329
387
 
@@ -335,14 +393,7 @@ def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
335
393
 
336
394
 
337
395
  def clear() -> None:
338
- base = os.path.dirname(os.path.abspath(__file__))
339
- poly_path = os.path.join(base, "poly")
340
- if os.path.exists(poly_path):
341
- shutil.rmtree(poly_path)
342
-
343
- vari_path = os.path.join(base, "vari")
344
- if os.path.exists(vari_path):
345
- shutil.rmtree(vari_path)
396
+ remove_old_library()
346
397
  print("Cleared!")
347
398
 
348
399
 
polyapi/poly_schemas.py CHANGED
@@ -121,7 +121,7 @@ def add_schema_file(
121
121
  # Read current __init__.py content if it exists
122
122
  init_content = ""
123
123
  if os.path.exists(init_path):
124
- with open(init_path, "r") as f:
124
+ with open(init_path, "r", encoding='utf-8') as f:
125
125
  init_content = f.read()
126
126
 
127
127
  # Prepare new content to append to __init__.py
@@ -129,12 +129,12 @@ def add_schema_file(
129
129
 
130
130
  # Use temporary files for atomic writes
131
131
  # Write to __init__.py atomically
132
- with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_init:
132
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp", encoding='utf-8') as temp_init:
133
133
  temp_init.write(new_init_content)
134
134
  temp_init_path = temp_init.name
135
135
 
136
136
  # Write to schema file atomically
137
- with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_schema:
137
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp", encoding='utf-8') as temp_schema:
138
138
  temp_schema.write(schema_defs)
139
139
  temp_schema_path = temp_schema.name
140
140
 
@@ -205,7 +205,7 @@ def create_schema(
205
205
  def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
206
206
  init_the_init(full_path, code_imports="")
207
207
  init_path = os.path.join(full_path, "__init__.py")
208
- with open(init_path, "a") as f:
208
+ with open(init_path, "a", encoding='utf-8') as f:
209
209
  f.write(render_poly_schema(spec) + "\n\n")
210
210
 
211
211
 
polyapi/poly_tables.py ADDED
@@ -0,0 +1,456 @@
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 scrub_keys(e: Exception) -> Dict[str, Any]:
11
+ """
12
+ Scrub the keys of an exception to remove sensitive information.
13
+ Returns a dictionary with the error message and type.
14
+ """
15
+ return {
16
+ "error": str(e),
17
+ "type": type(e).__name__,
18
+ "message": str(e),
19
+ "args": getattr(e, 'args', None)
20
+ }
21
+
22
+
23
+ def execute_query(table_id, method, query):
24
+ from polyapi import polyCustom
25
+ from polyapi.poly.client_id import client_id
26
+ try:
27
+ url = f"/tables/{table_id}/{method}?clientId={client_id}"
28
+ headers = {{
29
+ 'x-poly-execution-id': polyCustom.get('executionId')
30
+ }}
31
+ response = requests.post(url, json=query, headers=headers)
32
+ response.raise_for_status()
33
+ return response.json()
34
+ except Exception as e:
35
+ return scrub_keys(e)
36
+
37
+
38
+ def first_result(rsp):
39
+ if isinstance(rsp, dict) and isinstance(rsp.get('results'), list):
40
+ return rsp['results'][0] if rsp['results'] else None
41
+ return rsp
42
+
43
+
44
+ _key_transform_map = {
45
+ "not_": "not",
46
+ "in": "in",
47
+ "starts_with": "startsWith",
48
+ "ends_with": "startsWith",
49
+ "not_in": "notIn",
50
+ }
51
+
52
+
53
+ def _transform_keys(obj: Any) -> Any:
54
+ if isinstance(obj, dict):
55
+ return {
56
+ _key_transform_map.get(k, k): _transform_keys(v)
57
+ for k, v in obj.items()
58
+ }
59
+
60
+ elif isinstance(obj, list):
61
+ return [_transform_keys(v) for v in obj]
62
+
63
+ else:
64
+ return obj
65
+
66
+
67
+ def transform_query(query: dict) -> dict:
68
+ if query["where"] or query["order_by"]:
69
+ return {
70
+ **query,
71
+ "where": _transform_keys(query["where"]) if query["where"] else None,
72
+ "orderBy": query["order_by"] if query["order_by"] else None
73
+ }
74
+
75
+ return query
76
+
77
+
78
+ TABI_TABLE_TEMPLATE = '''
79
+ {table_name}Columns = Literal[{table_columns}]
80
+
81
+
82
+
83
+ {table_row_classes}
84
+
85
+
86
+
87
+ {table_row_subset_class}
88
+
89
+
90
+
91
+ {table_where_class}
92
+
93
+
94
+
95
+ class {table_name}SelectManyQuery(TypedDict):
96
+ where: NotRequired[{table_name}WhereFilter]
97
+ order_by: NotRequired[Dict[{table_name}Columns, SortOrder]]
98
+ limit: NotRequired[int]
99
+ offset: NotRequired[int]
100
+
101
+
102
+
103
+ class {table_name}SelectOneQuery(TypedDict):
104
+ where: NotRequired[{table_name}WhereFilter]
105
+ order_by: NotRequired[Dict[{table_name}Columns, SortOrder]]
106
+
107
+
108
+
109
+ class {table_name}InsertOneQuery(TypedDict):
110
+ data: {table_name}Subset
111
+
112
+
113
+
114
+ class {table_name}InsertManyQuery(TypedDict):
115
+ data: List[{table_name}Subset]
116
+
117
+
118
+
119
+ class {table_name}UpdateManyQuery(TypedDict):
120
+ where: NotRequired[{table_name}WhereFilter]
121
+ data: {table_name}Subset
122
+
123
+
124
+
125
+ class {table_name}DeleteQuery(TypedDict):
126
+ where: NotRequired[{table_name}WhereFilter]
127
+
128
+
129
+
130
+ class {table_name}QueryResults(TypedDict):
131
+ results: List[{table_name}Row]
132
+ pagination: None # Pagination not yet supported
133
+
134
+
135
+
136
+ class {table_name}CountQuery(TypedDict):
137
+ where: NotRequired[{table_name}WhereFilter]
138
+
139
+
140
+
141
+ class {table_name}:{table_description}
142
+ table_id = "{table_id}"
143
+
144
+ @overload
145
+ @staticmethod
146
+ def count(query: {table_name}CountQuery) -> PolyCountResult: ...
147
+ @overload
148
+ @staticmethod
149
+ def count(*, where: Optional[{table_name}WhereFilter]) -> PolyCountResult: ...
150
+
151
+ @staticmethod
152
+ def count(*args, **kwargs) -> PolyCountResult:
153
+ if args:
154
+ if len(args) != 1 or not isinstance(args[0], dict):
155
+ raise TypeError("Expected query as a single argument or as kwargs")
156
+ query = args[0]
157
+ else:
158
+ query = kwargs
159
+ return execute_query({table_name}.table_id, "count", transform_query(query))
160
+
161
+ @overload
162
+ @staticmethod
163
+ def select_many(query: {table_name}SelectManyQuery) -> {table_name}QueryResults: ...
164
+ @overload
165
+ @staticmethod
166
+ 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: ...
167
+
168
+ @staticmethod
169
+ def select_many(*args, **kwargs) -> {table_name}QueryResults:
170
+ if args:
171
+ if len(args) != 1 or not isinstance(args[0], dict):
172
+ raise TypeError("Expected query as a single argument or as kwargs")
173
+ query = args[0]
174
+ else:
175
+ query = kwargs
176
+ if query.get('limit') is None:
177
+ query['limit'] = 1000
178
+ if query['limit'] > 1000:
179
+ raise ValueError("Cannot select more than 1000 rows at a time.")
180
+ return execute_query({table_name}.table_id, "select", transform_query(query))
181
+
182
+ @overload
183
+ @staticmethod
184
+ def select_one(query: {table_name}SelectOneQuery) -> {table_name}Row: ...
185
+ @overload
186
+ @staticmethod
187
+ def select_one(*, where: Optional[{table_name}WhereFilter], order_by: Optional[Dict[{table_name}Columns, SortOrder]]) -> {table_name}Row: ...
188
+
189
+ @staticmethod
190
+ def select_one(*args, **kwargs) -> {table_name}Row:
191
+ if args:
192
+ if len(args) != 1 or not isinstance(args[0], dict):
193
+ raise TypeError("Expected query as a single argument or as kwargs")
194
+ query = args[0]
195
+ else:
196
+ query = kwargs
197
+ query['limit'] = 1
198
+ return first_result(execute_query({table_name}.table_id, "select", transform_query(query)))
199
+
200
+ @overload
201
+ @staticmethod
202
+ def insert_many(query: {table_name}InsertManyQuery) -> {table_name}QueryResults: ...
203
+ @overload
204
+ @staticmethod
205
+ def insert_many(*, data: List[{table_name}Subset]) -> {table_name}QueryResults: ...
206
+
207
+ @staticmethod
208
+ def insert_many(*args, **kwargs) -> {table_name}QueryResults:
209
+ if args:
210
+ if len(args) != 1 or not isinstance(args[0], dict):
211
+ raise TypeError("Expected query as a single argument or as kwargs")
212
+ query = args[0]
213
+ else:
214
+ query = kwargs
215
+ if len(query['data']) > 1000:
216
+ raise ValueError("Cannot insert more than 1000 rows at a time.")
217
+ return execute_query({table_name}.table_id, "insert", query)
218
+
219
+ @overload
220
+ @staticmethod
221
+ def insert_one(query: {table_name}InsertOneQuery) -> {table_name}Row: ...
222
+ @overload
223
+ @staticmethod
224
+ def insert_one(*, data: {table_name}Subset) -> {table_name}Row: ...
225
+
226
+ @staticmethod
227
+ def insert_one(*args, **kwargs) -> {table_name}Row:
228
+ if args:
229
+ if len(args) != 1 or not isinstance(args[0], dict):
230
+ raise TypeError("Expected query as a single argument or as kwargs")
231
+ query = args[0]
232
+ else:
233
+ query = kwargs
234
+ return first_result(execute_query({table_name}.table_id, "insert", {{ 'data': [query['data']] }}))
235
+
236
+ @overload
237
+ @staticmethod
238
+ def upsert_many(query: {table_name}InsertManyQuery) -> {table_name}QueryResults: ...
239
+ @overload
240
+ @staticmethod
241
+ def upsert_many(*, data: List[{table_name}Subset]) -> {table_name}QueryResults: ...
242
+
243
+ @staticmethod
244
+ def upsert_many(*args, **kwargs) -> {table_name}QueryResults:
245
+ if args:
246
+ if len(args) != 1 or not isinstance(args[0], dict):
247
+ raise TypeError("Expected query as a single argument or as kwargs")
248
+ query = args[0]
249
+ else:
250
+ query = kwargs
251
+ if len(data) > 1000:
252
+ raise ValueError("Cannot upsert more than 1000 rows at a time.")
253
+ return execute_query({table_name}.table_id, "upsert", query)
254
+
255
+ @overload
256
+ @staticmethod
257
+ def upsert_one(query: {table_name}InsertOneQuery) -> {table_name}Row: ...
258
+ @overload
259
+ @staticmethod
260
+ def upsert_one(*, data: {table_name}Subset) -> {table_name}Row: ...
261
+
262
+ @staticmethod
263
+ def upsert_one(*args, **kwargs) -> {table_name}Row:
264
+ if args:
265
+ if len(args) != 1 or not isinstance(args[0], dict):
266
+ raise TypeError("Expected query as a single argument or as kwargs")
267
+ query = args[0]
268
+ else:
269
+ query = kwargs
270
+ return first_result(execute_query({table_name}.table_id, "upsert", {{ 'data': [query['data']] }}))
271
+
272
+ @overload
273
+ @staticmethod
274
+ def update_many(query: {table_name}UpdateManyQuery) -> {table_name}QueryResults: ...
275
+ @overload
276
+ @staticmethod
277
+ def update_many(*, where: Optional[{table_name}WhereFilter], data: {table_name}Subset) -> {table_name}QueryResults: ...
278
+
279
+ @staticmethod
280
+ def update_many(*args, **kwargs) -> {table_name}QueryResults:
281
+ if args:
282
+ if len(args) != 1 or not isinstance(args[0], dict):
283
+ raise TypeError("Expected query as a single argument or as kwargs")
284
+ query = args[0]
285
+ else:
286
+ query = kwargs
287
+ return execute_query({table_name}.table_id, "update", transform_query(query))
288
+
289
+ @overload
290
+ @staticmethod
291
+ def delete_many(query: {table_name}DeleteQuery) -> PolyDeleteResults: ...
292
+ @overload
293
+ @staticmethod
294
+ def delete_many(*, where: Optional[{table_name}WhereFilter]) -> PolyDeleteResults: ...
295
+
296
+ @staticmethod
297
+ def delete_many(*args, **kwargs) -> PolyDeleteResults:
298
+ if args:
299
+ if len(args) != 1 or not isinstance(args[0], dict):
300
+ raise TypeError("Expected query as a single argument or as kwargs")
301
+ query = args[0]
302
+ else:
303
+ query = kwargs
304
+ return execute_query({table_name}.table_id, "delete", query)
305
+ '''
306
+
307
+
308
+ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -> str:
309
+ result = ""
310
+
311
+ col_type = schema.get("type", "object")
312
+ if isinstance(col_type, list):
313
+ subtypes = [_get_column_type_str(name, { **schema, "type": t }, is_required) for t in col_type]
314
+ result = f"Union[{', '.join(subtypes)}]"
315
+ elif col_type == "array":
316
+ if isinstance(schema["items"], list):
317
+ subtypes = [_get_column_type_str(f"{name}{i}", s, True) for i, s in enumerate(schema["items"])]
318
+ result = f"Tuple[{', '.join(subtypes)}]"
319
+ elif isinstance(schema["items"], dict):
320
+ result = f"List[{_get_column_type_str(name, schema['items'], True)}]"
321
+ else:
322
+ result = "List[Any]"
323
+ elif col_type == "object":
324
+ if isinstance(schema.get("patternProperties"), dict):
325
+ # TODO: Handle multiple pattern properties
326
+ result = f"Dict[str, {_get_column_type_str(f'{name}_', schema['patternProperties'], True)}]"
327
+ elif isinstance(schema.get("properties"), dict) and len(schema["properties"].values()) > 0:
328
+ # TODO: Handle x-poly-refs
329
+ result = f'"{name}"'
330
+ else:
331
+ result = "Dict[str, Any]"
332
+ else:
333
+ result = JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(schema["type"], "")
334
+
335
+ if result:
336
+ return result if is_required else f"Optional[{result}]"
337
+
338
+ return "Any"
339
+
340
+
341
+ def _render_table_row_classes(table_name: str, schema: Dict[str, Any]) -> str:
342
+ from polyapi.schema import wrapped_generate_schema_types
343
+
344
+ output = wrapped_generate_schema_types(schema, f"{table_name}Row", "Dict")
345
+
346
+ return output[1].split("\n", 1)[1].strip()
347
+
348
+
349
+ def _render_table_subset_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
350
+ # Generate class which can match any subset of a table row
351
+ lines = [f"class {table_name}Subset(TypedDict):"]
352
+
353
+ for name, schema in columns:
354
+ type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, name in required)
355
+ lines.append(f" {name}: NotRequired[{type_str}]")
356
+
357
+ return "\n".join(lines)
358
+
359
+
360
+ def _render_table_where_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
361
+ # Generate class for the 'where' part of the query
362
+ lines = [f"class {table_name}WhereFilter(TypedDict):"]
363
+
364
+ for name, schema in columns:
365
+ ftype_str = ""
366
+ type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, True) # force required to avoid wrapping type in Optional[]
367
+ is_required = name in required
368
+ if type_str == "bool":
369
+ ftype_str = "BooleanFilter" if is_required else "NullableBooleanFilter"
370
+ elif type_str == "str":
371
+ ftype_str = "StringFilter" if is_required else "NullableStringFilter"
372
+ elif type_str in ["int", "float"]:
373
+ ftype_str = "NumberFilter" if is_required else "NullableNumberFilter"
374
+ elif is_required == False:
375
+ type_str = "None"
376
+ ftype_str = "NullableObjectFilter"
377
+
378
+ if ftype_str:
379
+ lines.append(f" {name}: NotRequired[Union[{type_str}, {ftype_str}]]")
380
+
381
+ lines.append(f' AND: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
382
+ lines.append(f' OR: NotRequired[List["{table_name}WhereFilter"]]')
383
+ lines.append(f' NOT: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
384
+
385
+ return "\n".join(lines)
386
+
387
+
388
+ def _render_table(table: TableSpecDto) -> str:
389
+ columns = list(table["schema"]["properties"].items())
390
+ required_colunms = table["schema"].get("required", [])
391
+
392
+ table_columns = ",".join([ f'"{k}"' for k,_ in columns])
393
+ table_row_classes = _render_table_row_classes(table["name"], table["schema"])
394
+ table_row_subset_class = _render_table_subset_class(table["name"], columns, required_colunms)
395
+ table_where_class = _render_table_where_class(table["name"], columns, required_colunms)
396
+ if table.get("description", ""):
397
+ table_description = '\n """'
398
+ table_description += '\n '.join(table["description"].replace('"', "'").split("\n"))
399
+ table_description += '\n """'
400
+ else:
401
+ table_description = ""
402
+
403
+ return TABI_TABLE_TEMPLATE.format(
404
+ table_name=table["name"],
405
+ table_id=table["id"],
406
+ table_description=table_description,
407
+ table_columns=table_columns,
408
+ table_row_classes=table_row_classes,
409
+ table_row_subset_class=table_row_subset_class,
410
+ table_where_class=table_where_class,
411
+ )
412
+
413
+
414
+ def generate_tables(tables: List[TableSpecDto]):
415
+ for table in tables:
416
+ _create_table(table)
417
+
418
+
419
+ def _create_table(table: TableSpecDto) -> None:
420
+ folders = ["tabi"]
421
+ if table["context"]:
422
+ folders += table["context"].split(".")
423
+
424
+ # build up the full_path by adding all the folders
425
+ base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)))
426
+ full_path = base_path
427
+
428
+ for idx, folder in enumerate(folders):
429
+ full_path = os.path.join(full_path, folder)
430
+ if not os.path.exists(full_path):
431
+ os.makedirs(full_path)
432
+ next = folders[idx + 1] if idx + 1 < len(folders) else None
433
+ if next:
434
+ add_import_to_init(full_path, next, "")
435
+
436
+ init_path = os.path.join(full_path, "__init__.py")
437
+
438
+ imports = "\n".join([
439
+ "from typing_extensions import NotRequired, TypedDict",
440
+ "from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
441
+ "from polyapi.poly_tables import execute_query, first_result, transform_query",
442
+ "from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
443
+ ])
444
+ table_contents = _render_table(table)
445
+
446
+ file_contents = ""
447
+ if os.path.exists(init_path):
448
+ with open(init_path, "r") as f:
449
+ file_contents = f.read()
450
+
451
+ with open(init_path, "w") as f:
452
+ if not file_contents.startswith(imports):
453
+ f.write(imports + "\n\n\n")
454
+ if file_contents:
455
+ f.write(file_contents + "\n\n\n")
456
+ f.write(table_contents)
polyapi/prepare.py CHANGED
@@ -138,11 +138,13 @@ def prepare_deployables(lazy: bool = False, disable_docs: bool = False, disable_
138
138
  write_updated_deployable(deployable, disable_docs)
139
139
  # Re-stage any updated staged files.
140
140
  staged = subprocess.check_output('git diff --name-only --cached', shell=True, text=True, ).split('\n')
141
+ rootPath = subprocess.check_output('git rev-parse --show-toplevel', shell=True, text=True).replace('\n', '')
141
142
  for deployable in dirty_deployables:
142
143
  try:
143
- if deployable["file"] in staged:
144
- print(f'Staging {deployable["file"]}')
145
- subprocess.run(['git', 'add', deployable["file"]])
144
+ deployableName = deployable["file"].replace('\\', '/').replace(f"{rootPath}/", '')
145
+ if deployableName in staged:
146
+ print(f'Staging {deployableName}')
147
+ subprocess.run(['git', 'add', deployableName])
146
148
  except:
147
149
  print('Warning: File staging failed, check that all files are staged properly.')
148
150
 
polyapi/schema.py CHANGED
@@ -93,7 +93,7 @@ def generate_schema_types(input_data: Dict, root=None):
93
93
  with contextlib.redirect_stdout(None):
94
94
  process_config(config, [tmp_input])
95
95
 
96
- with open(tmp_output) as f:
96
+ with open(tmp_output, encoding='utf-8') as f:
97
97
  output = f.read()
98
98
 
99
99
  output = clean_malformed_examples(output)
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
@@ -23,19 +24,21 @@ def read_file(file_path: str) -> str:
23
24
  return file.read()
24
25
 
25
26
  def group_by(items: List[Dict], key: str) -> Dict[str, List[Dict]]:
26
- grouped = {}
27
+ grouped = {} # type: ignore
27
28
  for item in items:
28
29
  grouped.setdefault(item[key], []).append(item)
29
30
  return grouped
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 Exception("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.8.dev10
3
+ Version: 0.3.9
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
@@ -32,10 +32,10 @@ Requires-Dist: requests>=2.32.3
32
32
  Requires-Dist: typing_extensions>=4.12.2
33
33
  Requires-Dist: jsonschema-gentypes==2.6.0
34
34
  Requires-Dist: pydantic>=2.8.0
35
- Requires-Dist: stdlib_list==0.10.0
35
+ Requires-Dist: stdlib_list>=0.10.0
36
36
  Requires-Dist: colorama==0.4.4
37
37
  Requires-Dist: python-socketio[asyncio_client]==5.11.1
38
- Requires-Dist: truststore==0.8.0
38
+ Requires-Dist: truststore>=0.8.0
39
39
  Dynamic: license-file
40
40
 
41
41
  # PolyAPI Python Library
@@ -110,24 +110,6 @@ def bar():
110
110
  return "Hello World"
111
111
  ```
112
112
 
113
- ## See Server Function Logs
114
-
115
- In order to see function logs, please first set `logsEnabled` to `true` in Canopy for the function.
116
-
117
- https://na1.polyapi.io/canopy/polyui/collections/server-functions
118
-
119
- Then in your code, get the poly logger and log with it like so:
120
-
121
- ```python
122
- logger = logging.getLogger("poly")
123
- def bar():
124
- logger.warning("I AM THE LOG")
125
- return "Hello World"
126
- ```
127
-
128
- Finally, click the "Show Logs" button to see your server function logs in Canopy!
129
-
130
-
131
113
  ## Complex Types In Server Functions
132
114
 
133
115
  You can define arbitrarily complex argument and return types using TypedDicts.
@@ -0,0 +1,32 @@
1
+ polyapi/__init__.py,sha256=hw7x4j9JNJfPdkIOZqV0X9pbYcw3_5AH1iQFdSogH-c,3235
2
+ polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
3
+ polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
4
+ polyapi/auth.py,sha256=EGstBjTSdAydI5hGAHeRRc1GcmHshogudb3sxCgO6zA,5341
5
+ polyapi/cli.py,sha256=unKqAoZ1hTGAeyYRfNQ6jO15Um7N4F95k__1qFue5bI,10659
6
+ polyapi/client.py,sha256=DW6ljG_xCwAo2yz23A9QfLooE6ZUDvSpdA4e_dCQjiQ,1418
7
+ polyapi/config.py,sha256=cAMv2n9tGN_BTvqt7V32o5F86qRhxAKyey_PoId2D8s,7638
8
+ polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
9
+ polyapi/deployables.py,sha256=6R7XSgpTohZBcqoGd7GioQdXXKuvbBsdq_cAJ1p8jfQ,12184
10
+ polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
11
+ polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
12
+ polyapi/execute.py,sha256=q4xtV6rYO8f-8hULlFTlVgoTVQSahvlMu3FHkFzYpMs,4423
13
+ polyapi/function_cli.py,sha256=H0sVrbvRBXw_xeApe2MvQw8p_xE7jVTTOU-07Dg041A,4220
14
+ polyapi/generate.py,sha256=thOrjq4suOkR97x00rthor96aslzzOaAAo4yxLtiuh0,21081
15
+ polyapi/parser.py,sha256=20ZE7kSXx3UL7QVSIYYxzsnJlygVbsaDAg9q7c41WxQ,20695
16
+ polyapi/poly_schemas.py,sha256=fZ6AGvHcOKQJtlrzSuzeBNed5DxPMA2dJGdJvuFCHWM,9066
17
+ polyapi/poly_tables.py,sha256=adftBHGncvxpPchpjfFwDjQncVn9lZTrRHNroei4Nw0,15897
18
+ polyapi/prepare.py,sha256=NQzpMIoakNovStvOGOmqSYIpTwiWXaweNSE9se10A2E,7420
19
+ polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
21
+ polyapi/schema.py,sha256=-mtRV5iL3CV0X3phXhGYFV8sLz0KouTACOKWyGO9Pwc,5309
22
+ polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
23
+ polyapi/sync.py,sha256=52ODc82jBJpbNYTB9zXlrVZLR39iwDPW3cuIC3P8dbM,6742
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.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
29
+ polyapi_python-0.3.9.dist-info/METADATA,sha256=cHzEKzBAWYYV4Dm62Xk95_kSZuN9lpZj_rwZBc9N5rA,5312
30
+ polyapi_python-0.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ polyapi_python-0.3.9.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
32
+ polyapi_python-0.3.9.dist-info/RECORD,,
@@ -1,31 +0,0 @@
1
- polyapi/__init__.py,sha256=hw7x4j9JNJfPdkIOZqV0X9pbYcw3_5AH1iQFdSogH-c,3235
2
- polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
3
- polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
4
- polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
5
- polyapi/cli.py,sha256=unKqAoZ1hTGAeyYRfNQ6jO15Um7N4F95k__1qFue5bI,10659
6
- polyapi/client.py,sha256=DW6ljG_xCwAo2yz23A9QfLooE6ZUDvSpdA4e_dCQjiQ,1418
7
- polyapi/config.py,sha256=cAMv2n9tGN_BTvqt7V32o5F86qRhxAKyey_PoId2D8s,7638
8
- polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
9
- polyapi/deployables.py,sha256=8x-Y7MlpWnTqsWhidmO6yVZe2nTChzma6hJNVsfg94s,12113
10
- polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
11
- polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
12
- polyapi/execute.py,sha256=sjI6BMBYPSCD6UngV9DzpJIRSU6p02aShNaTXhDExtY,3457
13
- polyapi/function_cli.py,sha256=H0sVrbvRBXw_xeApe2MvQw8p_xE7jVTTOU-07Dg041A,4220
14
- polyapi/generate.py,sha256=slCw9AOvQHQ8UtEaumFI1NoRvjH2Dj3Y33u7imQqi8c,19521
15
- polyapi/parser.py,sha256=20ZE7kSXx3UL7QVSIYYxzsnJlygVbsaDAg9q7c41WxQ,20695
16
- polyapi/poly_schemas.py,sha256=760g-rBou-XT0y7N0THHHJBHlqIrOhVsAj500T6Qp0Q,8994
17
- polyapi/prepare.py,sha256=pRWBhpgqMtKP04P9F6PIA3eCkOpCxQSv9TZdR3qR34I,7216
18
- polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
20
- polyapi/schema.py,sha256=zMN0zr_dku-NzmL8e0C6lbym8I0SrzXxtU5I8-YwHkM,5291
21
- 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.8.dev10.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
28
- polyapi_python-0.3.8.dev10.dist-info/METADATA,sha256=gEaVycjq9LZDUwCA43SyffH-G52cN1CnB1OP004FHz8,5785
29
- polyapi_python-0.3.8.dev10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- polyapi_python-0.3.8.dev10.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
31
- polyapi_python-0.3.8.dev10.dist-info/RECORD,,