polyapi-python 0.0.34__tar.gz → 0.1.0__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.
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/PKG-INFO +11 -1
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/README.md +8 -0
- polyapi-python-0.1.0/polyapi/api.py +57 -0
- polyapi-python-0.1.0/polyapi/auth.py +151 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/cli.py +16 -7
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/config.py +9 -0
- polyapi-python-0.1.0/polyapi/execute.py +46 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/function_cli.py +26 -10
- polyapi-python-0.1.0/polyapi/generate.py +243 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/schema.py +9 -2
- polyapi-python-0.1.0/polyapi/server.py +64 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/typedefs.py +18 -2
- polyapi-python-0.1.0/polyapi/utils.py +154 -0
- polyapi-python-0.1.0/polyapi/variables.py +102 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi_python.egg-info/PKG-INFO +11 -1
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi_python.egg-info/SOURCES.txt +7 -2
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi_python.egg-info/requires.txt +2 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/pyproject.toml +3 -3
- polyapi-python-0.0.34/tests/test_generate.py → polyapi-python-0.1.0/tests/test_api.py +108 -53
- polyapi-python-0.1.0/tests/test_auth.py +139 -0
- polyapi-python-0.1.0/tests/test_server.py +62 -0
- polyapi-python-0.1.0/tests/test_variables.py +28 -0
- polyapi-python-0.0.34/polyapi/api.py +0 -290
- polyapi-python-0.0.34/polyapi/execute.py +0 -15
- polyapi-python-0.0.34/polyapi/generate.py +0 -148
- polyapi-python-0.0.34/polyapi/utils.py +0 -26
- polyapi-python-0.0.34/polyapi/variables.py +0 -86
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/LICENSE +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/__init__.py +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/__main__.py +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/constants.py +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/exceptions.py +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi/py.typed +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/setup.cfg +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/tests/test_function_cli.py +0 -0
- {polyapi-python-0.0.34 → polyapi-python-0.1.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: The PolyAPI Python Client
|
|
5
5
|
Author-email: Dan Fellin <dan@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
@@ -33,6 +33,8 @@ Requires-Dist: typing_extensions
|
|
|
33
33
|
Requires-Dist: jsonschema-gentypes
|
|
34
34
|
Requires-Dist: pydantic>=2.5.3
|
|
35
35
|
Requires-Dist: stdlib_list
|
|
36
|
+
Requires-Dist: colorama
|
|
37
|
+
Requires-Dist: python-socketio[asyncio_client]
|
|
36
38
|
|
|
37
39
|
# PolyAPI Python Library
|
|
38
40
|
|
|
@@ -145,6 +147,14 @@ To upgrade your library to the latest dev version, pass the `--pre` flag.
|
|
|
145
147
|
pip install polyapi-python --pre --upgrade
|
|
146
148
|
```
|
|
147
149
|
|
|
150
|
+
## Change Your API Key
|
|
151
|
+
|
|
152
|
+
If you need to change your API key or what server you are pointing to, you can run:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
python -m polyapi setup
|
|
156
|
+
```
|
|
157
|
+
|
|
148
158
|
## Unit Tests
|
|
149
159
|
|
|
150
160
|
To run this library's unit tests, please clone the repo then run:
|
|
@@ -109,6 +109,14 @@ To upgrade your library to the latest dev version, pass the `--pre` flag.
|
|
|
109
109
|
pip install polyapi-python --pre --upgrade
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
## Change Your API Key
|
|
113
|
+
|
|
114
|
+
If you need to change your API key or what server you are pointing to, you can run:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
python -m polyapi setup
|
|
118
|
+
```
|
|
119
|
+
|
|
112
120
|
## Unit Tests
|
|
113
121
|
|
|
114
122
|
To run this library's unit tests, please clone the repo then run:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Tuple
|
|
2
|
+
|
|
3
|
+
from polyapi.typedefs import PropertySpecification
|
|
4
|
+
from polyapi.utils import add_type_import_path, camelCase, parse_arguments, get_type_and_def
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
API_DEFS_TEMPLATE = """
|
|
8
|
+
from typing import List, Dict, Any, TypedDict
|
|
9
|
+
{args_def}
|
|
10
|
+
{return_type_def}
|
|
11
|
+
class {api_response_type}(TypedDict):
|
|
12
|
+
status: int
|
|
13
|
+
headers: Dict
|
|
14
|
+
data: {return_type_name}
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
API_FUNCTION_TEMPLATE = """
|
|
18
|
+
def {function_name}(
|
|
19
|
+
{args}
|
|
20
|
+
) -> {api_response_type}:
|
|
21
|
+
"{function_description}"
|
|
22
|
+
resp = execute("{function_type}", "{function_id}", {data})
|
|
23
|
+
return {api_response_type}(resp.json()) # type: ignore
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_api_function(
|
|
28
|
+
function_type: str,
|
|
29
|
+
function_name: str,
|
|
30
|
+
function_id: str,
|
|
31
|
+
function_description: str,
|
|
32
|
+
arguments: List[PropertySpecification],
|
|
33
|
+
return_type: Dict[str, Any],
|
|
34
|
+
) -> Tuple[str, str]:
|
|
35
|
+
assert function_type == "apiFunction"
|
|
36
|
+
arg_names = [a["name"] for a in arguments]
|
|
37
|
+
args, args_def = parse_arguments(function_name, arguments)
|
|
38
|
+
return_type_name, return_type_def = get_type_and_def(return_type) # type: ignore
|
|
39
|
+
data = "{" + ", ".join([f"'{arg}': {camelCase(arg)}" for arg in arg_names]) + "}"
|
|
40
|
+
|
|
41
|
+
api_response_type = f"{function_name}Response"
|
|
42
|
+
func_type_defs = API_DEFS_TEMPLATE.format(
|
|
43
|
+
args_def=args_def,
|
|
44
|
+
api_response_type=api_response_type,
|
|
45
|
+
return_type_name=return_type_name,
|
|
46
|
+
return_type_def=return_type_def,
|
|
47
|
+
)
|
|
48
|
+
func_str = API_FUNCTION_TEMPLATE.format(
|
|
49
|
+
function_type="api",
|
|
50
|
+
function_name=function_name,
|
|
51
|
+
function_id=function_id,
|
|
52
|
+
function_description=function_description.replace('"', "'"),
|
|
53
|
+
args=args,
|
|
54
|
+
data=data,
|
|
55
|
+
api_response_type=add_type_import_path(function_name, api_response_type),
|
|
56
|
+
)
|
|
57
|
+
return func_str, func_type_defs
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from typing import List, Dict, Any, Tuple
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from polyapi.typedefs import PropertySpecification
|
|
5
|
+
from polyapi.utils import parse_arguments, get_type_and_def
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
AUTH_DEFS_TEMPLATE = """
|
|
9
|
+
from typing import List, Dict, Any, TypedDict, Optional
|
|
10
|
+
{args_def}
|
|
11
|
+
{return_type_def}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
GET_TOKEN_TEMPLATE = """
|
|
15
|
+
import asyncio
|
|
16
|
+
import socketio # type: ignore
|
|
17
|
+
from polyapi.config import get_api_key_and_url
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def getToken(clientId: str, clientSecret: str, scopes: List[str], callback, options: Optional[Dict[str, Any]] = None):
|
|
21
|
+
{description}
|
|
22
|
+
eventsClientId = "{client_id}"
|
|
23
|
+
function_id = "{function_id}"
|
|
24
|
+
|
|
25
|
+
options = options or {{}}
|
|
26
|
+
path = "/auth-providers/{function_id}/execute"
|
|
27
|
+
data = {{
|
|
28
|
+
"clientId": clientId,
|
|
29
|
+
"clientSecret": clientSecret,
|
|
30
|
+
"scopes": scopes,
|
|
31
|
+
"audience": options.get("audience"),
|
|
32
|
+
"callbackUrl": options.get("callbackUrl"),
|
|
33
|
+
"userId": options.get("userId"),
|
|
34
|
+
}}
|
|
35
|
+
resp = execute_post(path, data)
|
|
36
|
+
data = resp.json()
|
|
37
|
+
assert resp.status_code == 201, (resp.status_code, resp.content)
|
|
38
|
+
|
|
39
|
+
token = data.get("token")
|
|
40
|
+
url = data.get("url")
|
|
41
|
+
error = data.get("error")
|
|
42
|
+
if token:
|
|
43
|
+
return callback(token, url, error)
|
|
44
|
+
elif url and options.get("autoCloseOnUrl"):
|
|
45
|
+
return callback(token, url, error)
|
|
46
|
+
|
|
47
|
+
timeout = options.get("timeout", 120)
|
|
48
|
+
|
|
49
|
+
api_key, base_url = get_api_key_and_url()
|
|
50
|
+
socket = socketio.AsyncClient()
|
|
51
|
+
await socket.connect(base_url, transports=['websocket'], namespaces=['/events'])
|
|
52
|
+
|
|
53
|
+
async def closeEventHandler():
|
|
54
|
+
nonlocal socket
|
|
55
|
+
if not socket:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
del socket.handlers['/events']['handleAuthFunctionEvent:{function_id}']
|
|
59
|
+
await socket.emit('unregisterAuthFunctionEventHandler', {{
|
|
60
|
+
"clientID": eventsClientId,
|
|
61
|
+
"functionId": function_id,
|
|
62
|
+
"apiKey": api_key
|
|
63
|
+
}}, namespace="/events")
|
|
64
|
+
await socket.disconnect()
|
|
65
|
+
socket = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def waitUntilTimeout(timeout):
|
|
69
|
+
await asyncio.sleep(timeout)
|
|
70
|
+
await closeEventHandler()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def handleEvent(data):
|
|
74
|
+
nonlocal options
|
|
75
|
+
callback(data.get('token'), data.get('url'), data.get('error'))
|
|
76
|
+
if data.get('token') and options.get("autoCloseOnToken", True):
|
|
77
|
+
await closeEventHandler()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def registerCallback(registered: bool):
|
|
81
|
+
nonlocal socket
|
|
82
|
+
if registered:
|
|
83
|
+
socket.on('handleAuthFunctionEvent:{function_id}', handleEvent, namespace="/events")
|
|
84
|
+
callback(data.get('token'), data.get('url'), data.get('error'))
|
|
85
|
+
|
|
86
|
+
data2 = {{
|
|
87
|
+
"clientID": eventsClientId,
|
|
88
|
+
"functionId": function_id,
|
|
89
|
+
"apiKey": api_key
|
|
90
|
+
}}
|
|
91
|
+
await socket.emit('registerAuthFunctionEventHandler', data2, namespace="/events", callback=registerCallback)
|
|
92
|
+
|
|
93
|
+
# run timeout task in background
|
|
94
|
+
timeout = options.get("timeout", 120)
|
|
95
|
+
timeout_task = asyncio.create_task(waitUntilTimeout(timeout))
|
|
96
|
+
|
|
97
|
+
# cancel timeout task if socket.wait finishes before timeout up
|
|
98
|
+
await socket.wait()
|
|
99
|
+
timeout_task.cancel()
|
|
100
|
+
|
|
101
|
+
return {{"close": closeEventHandler}}
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
REFRESH_TOKEN_TEMPLATE = """
|
|
105
|
+
def refreshToken(token: str) -> str:
|
|
106
|
+
{description}
|
|
107
|
+
url = "/auth-providers/{function_id}/refresh"
|
|
108
|
+
resp = execute_post(url, {{"token": token}})
|
|
109
|
+
assert resp.status_code == 201, (resp.status_code, resp.content)
|
|
110
|
+
return resp.text
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
REVOKE_TOKEN_TEMPLATE = """
|
|
114
|
+
def revokeToken(token: str) -> None:
|
|
115
|
+
{description}
|
|
116
|
+
url = "/auth-providers/{function_id}/revoke"
|
|
117
|
+
resp = execute_post(url, {{"token": token}})
|
|
118
|
+
assert resp.status_code == 201, (resp.status_code, resp.content)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_auth_function(
|
|
123
|
+
function_type: str,
|
|
124
|
+
function_name: str,
|
|
125
|
+
function_id: str,
|
|
126
|
+
function_description: str,
|
|
127
|
+
arguments: List[PropertySpecification],
|
|
128
|
+
return_type: Dict[str, Any],
|
|
129
|
+
) -> Tuple[str, str]:
|
|
130
|
+
""" renders getToken, revokeToken, refreshToken as appropriate
|
|
131
|
+
"""
|
|
132
|
+
args, args_def = parse_arguments(function_name, arguments)
|
|
133
|
+
return_type_name, return_type_def = get_type_and_def(return_type) # type: ignore
|
|
134
|
+
func_type_defs = AUTH_DEFS_TEMPLATE.format(
|
|
135
|
+
args_def=args_def,
|
|
136
|
+
return_type_def=return_type_def,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
func_str = ""
|
|
140
|
+
|
|
141
|
+
if function_description:
|
|
142
|
+
function_description = f'"""{function_description}"""'
|
|
143
|
+
|
|
144
|
+
if function_name == "getToken":
|
|
145
|
+
func_str = GET_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description, client_id=uuid.uuid4().hex)
|
|
146
|
+
elif function_name == "refreshToken":
|
|
147
|
+
func_str = REFRESH_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description)
|
|
148
|
+
elif function_name == "revokeToken":
|
|
149
|
+
func_str = REVOKE_TOKEN_TEMPLATE.format(function_id=function_id, description=function_description)
|
|
150
|
+
|
|
151
|
+
return func_str, func_type_defs
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
|
|
3
|
+
from polyapi.utils import print_green
|
|
4
|
+
|
|
3
5
|
from .config import clear_config
|
|
4
6
|
from .generate import generate, clear
|
|
5
7
|
from .function_cli import function_add_or_update
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
CLI_COMMANDS = ["
|
|
10
|
+
CLI_COMMANDS = ["setup", "generate", "function", "clear", "help"]
|
|
11
|
+
|
|
12
|
+
CLIENT_DESC = """Commands
|
|
13
|
+
python -m polyapi setup Setup your Poly connection
|
|
14
|
+
python -m polyapi generate Generates Poly library
|
|
15
|
+
python -m polyapi function <command> Manages functions
|
|
16
|
+
python -m polyapi clear Clear current generated Poly library
|
|
17
|
+
"""
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
def execute_from_cli():
|
|
12
21
|
parser = argparse.ArgumentParser(
|
|
13
|
-
prog="python -m polyapi", description=
|
|
22
|
+
prog="python -m polyapi", description=CLIENT_DESC, formatter_class=argparse.RawTextHelpFormatter
|
|
14
23
|
)
|
|
15
24
|
parser.add_argument("--context", required=False, default="")
|
|
16
25
|
parser.add_argument("--description", required=False, default="")
|
|
17
|
-
parser.add_argument("--server", action="store_true", help="Pass --server
|
|
18
|
-
parser.add_argument("--logs", action="store_true", help="Pass --logs if you want to store and see the
|
|
26
|
+
parser.add_argument("--server", action="store_true", help="Pass --server when adding function to add a server function. By default, new functions are client.")
|
|
27
|
+
parser.add_argument("--logs", action="store_true", help="Pass --logs when adding function if you want to store and see the function logs.")
|
|
19
28
|
parser.add_argument("command", choices=CLI_COMMANDS)
|
|
20
29
|
parser.add_argument("subcommands", nargs="*")
|
|
21
30
|
args = parser.parse_args()
|
|
@@ -24,10 +33,10 @@ def execute_from_cli():
|
|
|
24
33
|
if command == "help":
|
|
25
34
|
parser.print_help()
|
|
26
35
|
elif command == "generate":
|
|
27
|
-
print("Generating...")
|
|
36
|
+
print("Generating Poly functions...", end="")
|
|
28
37
|
generate()
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
print_green("DONE")
|
|
39
|
+
elif command == "setup":
|
|
31
40
|
clear_config()
|
|
32
41
|
generate()
|
|
33
42
|
elif command == "clear":
|
|
@@ -69,6 +69,15 @@ def initialize_config():
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def clear_config():
|
|
72
|
+
if os.environ.get("POLY_API_KEY"):
|
|
73
|
+
print("Using POLY_API_KEY from environment. Please unset environment variable to manually set api key.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
global API_KEY
|
|
77
|
+
global API_URL
|
|
78
|
+
API_KEY = None
|
|
79
|
+
API_URL = None
|
|
80
|
+
|
|
72
81
|
path = get_config_file_path()
|
|
73
82
|
if os.path.exists(path):
|
|
74
83
|
os.remove(path)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from requests import Response
|
|
3
|
+
from polyapi.config import get_api_key_and_url
|
|
4
|
+
from polyapi.exceptions import PolyApiException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def execute(function_type, function_id, data) -> Response:
|
|
8
|
+
""" execute a specific function id/type
|
|
9
|
+
"""
|
|
10
|
+
api_key, api_url = get_api_key_and_url()
|
|
11
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
12
|
+
url = f"{api_url}/functions/{function_type}/{function_id}/execute"
|
|
13
|
+
resp = requests.post(url, json=data, headers=headers)
|
|
14
|
+
if resp.status_code != 200 and resp.status_code != 201:
|
|
15
|
+
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
16
|
+
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
17
|
+
return resp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execute_post(path, data):
|
|
21
|
+
api_key, api_url = get_api_key_and_url()
|
|
22
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
23
|
+
resp = requests.post(api_url + path, json=data, headers=headers)
|
|
24
|
+
return resp
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def variable_get(variable_id: str) -> Response:
|
|
28
|
+
api_key, base_url = get_api_key_and_url()
|
|
29
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
30
|
+
url = f"{base_url}/variables/{variable_id}/value"
|
|
31
|
+
resp = requests.get(url, headers=headers)
|
|
32
|
+
if resp.status_code != 200 and resp.status_code != 201:
|
|
33
|
+
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
34
|
+
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
35
|
+
return resp
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def variable_update(variable_id: str, value) -> Response:
|
|
39
|
+
api_key, base_url = get_api_key_and_url()
|
|
40
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
41
|
+
url = f"{base_url}/variables/{variable_id}/value"
|
|
42
|
+
resp = requests.patch(url, data={"value": value}, headers=headers)
|
|
43
|
+
if resp.status_code != 200 and resp.status_code != 201:
|
|
44
|
+
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
45
|
+
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
46
|
+
return resp
|
|
@@ -8,15 +8,16 @@ from typing_extensions import _TypedDictMeta # type: ignore
|
|
|
8
8
|
import requests
|
|
9
9
|
from stdlib_list import stdlib_list
|
|
10
10
|
from pydantic import TypeAdapter
|
|
11
|
-
from polyapi.generate import
|
|
11
|
+
from polyapi.generate import get_functions_and_parse, generate_functions
|
|
12
12
|
from polyapi.config import get_api_key_and_url
|
|
13
13
|
from polyapi.constants import PYTHON_TO_JSONSCHEMA_TYPE_MAP
|
|
14
|
-
from polyapi.utils import get_auth_headers
|
|
14
|
+
from polyapi.utils import get_auth_headers, print_green, print_red, print_yellow
|
|
15
|
+
import importlib
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
# these libraries are already installed in the base docker image
|
|
18
19
|
# and shouldnt be included in additional requirements
|
|
19
|
-
BASE_REQUIREMENTS = {"requests", "typing_extensions", "jsonschema-gentypes", "pydantic"}
|
|
20
|
+
BASE_REQUIREMENTS = {"polyapi", "requests", "typing_extensions", "jsonschema-gentypes", "pydantic"}
|
|
20
21
|
all_stdlib_symbols = stdlib_list('.'.join([str(v) for v in sys.version_info[0:2]]))
|
|
21
22
|
BASE_REQUIREMENTS.update(all_stdlib_symbols) # dont need to pip install stuff in the python standard library
|
|
22
23
|
|
|
@@ -141,6 +142,14 @@ def _parse_code(code: str, function_name: str):
|
|
|
141
142
|
return parsed_args, return_type, return_type_schema, requirements
|
|
142
143
|
|
|
143
144
|
|
|
145
|
+
def _func_already_exists(context: str, function_name: str) -> bool:
|
|
146
|
+
try:
|
|
147
|
+
module = importlib.import_module(f"polyapi.poly.{context}")
|
|
148
|
+
return bool(getattr(module, function_name, False))
|
|
149
|
+
except ModuleNotFoundError:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
144
153
|
def function_add_or_update(
|
|
145
154
|
context: str, description: str, server: bool, logs_enabled: bool, subcommands: List
|
|
146
155
|
):
|
|
@@ -150,6 +159,9 @@ def function_add_or_update(
|
|
|
150
159
|
parser.add_argument("filename")
|
|
151
160
|
args = parser.parse_args(subcommands)
|
|
152
161
|
|
|
162
|
+
verb = "Updating" if _func_already_exists(context, args.function_name) else "Adding"
|
|
163
|
+
print(f"{verb} custom server side function...", end="")
|
|
164
|
+
|
|
153
165
|
with open(args.filename, "r") as f:
|
|
154
166
|
code = f.read()
|
|
155
167
|
|
|
@@ -162,11 +174,13 @@ def function_add_or_update(
|
|
|
162
174
|
) = _parse_code(code, args.function_name)
|
|
163
175
|
|
|
164
176
|
if not return_type:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
177
|
+
print_red("ERROR")
|
|
178
|
+
print(f"Function {args.function_name} not found as top-level function in {args.filename}")
|
|
168
179
|
sys.exit(1)
|
|
169
180
|
|
|
181
|
+
if requirements:
|
|
182
|
+
print_yellow('\nPlease note that deploying your functions will take a few minutes because it makes use of libraries other than polyapi.')
|
|
183
|
+
|
|
170
184
|
data = {
|
|
171
185
|
"context": context,
|
|
172
186
|
"name": args.function_name,
|
|
@@ -189,13 +203,15 @@ def function_add_or_update(
|
|
|
189
203
|
# url = f"{base_url}/functions/client"
|
|
190
204
|
|
|
191
205
|
headers = get_auth_headers(api_key)
|
|
192
|
-
print("Adding function...")
|
|
193
206
|
resp = requests.post(url, headers=headers, json=data)
|
|
194
207
|
if resp.status_code == 201:
|
|
208
|
+
print_green("DEPLOYED")
|
|
195
209
|
function_id = resp.json()["id"]
|
|
196
|
-
print(f"Function
|
|
197
|
-
print("
|
|
198
|
-
|
|
210
|
+
print(f"Function ID: {function_id}")
|
|
211
|
+
print("Generating new custom function...", end="")
|
|
212
|
+
functions = get_functions_and_parse(limit_ids=[function_id])
|
|
213
|
+
generate_functions(functions)
|
|
214
|
+
print_green("DONE")
|
|
199
215
|
else:
|
|
200
216
|
print("Error adding function.")
|
|
201
217
|
print(resp.status_code)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from typing import Any, Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
from polyapi.auth import render_auth_function
|
|
8
|
+
|
|
9
|
+
from .typedefs import PropertySpecification, SpecificationDto, VariableSpecDto
|
|
10
|
+
from .api import render_api_function
|
|
11
|
+
from .server import render_server_function
|
|
12
|
+
from .utils import add_import_to_init, get_auth_headers, init_the_init
|
|
13
|
+
from .variables import generate_variables
|
|
14
|
+
from .config import get_api_key_and_url, initialize_config
|
|
15
|
+
|
|
16
|
+
SUPPORTED_FUNCTION_TYPES = {
|
|
17
|
+
"apiFunction",
|
|
18
|
+
"authFunction",
|
|
19
|
+
"serverFunction",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_specs() -> List:
|
|
26
|
+
api_key, api_url = get_api_key_and_url()
|
|
27
|
+
assert api_key
|
|
28
|
+
headers = get_auth_headers(api_key)
|
|
29
|
+
url = f"{api_url}/specs"
|
|
30
|
+
resp = requests.get(url, headers=headers)
|
|
31
|
+
if resp.status_code == 200:
|
|
32
|
+
return resp.json()
|
|
33
|
+
else:
|
|
34
|
+
raise NotImplementedError(resp.content)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parse_function_specs(
|
|
38
|
+
specs: List,
|
|
39
|
+
limit_ids: List[str] | None # optional list of ids to limit to
|
|
40
|
+
) -> List[Tuple[str, str, str, str, List[PropertySpecification], Dict[str, Any]]]:
|
|
41
|
+
functions = []
|
|
42
|
+
for spec in specs:
|
|
43
|
+
if limit_ids and spec["id"] not in limit_ids:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
if spec["type"] not in SUPPORTED_FUNCTION_TYPES:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
function_type = spec["type"]
|
|
50
|
+
function_name = f"poly.{spec['context']}.{spec['name']}"
|
|
51
|
+
function_id = spec["id"]
|
|
52
|
+
arguments: List[PropertySpecification] = [
|
|
53
|
+
arg for arg in spec["function"]["arguments"]
|
|
54
|
+
]
|
|
55
|
+
functions.append(
|
|
56
|
+
(
|
|
57
|
+
function_type,
|
|
58
|
+
function_name,
|
|
59
|
+
function_id,
|
|
60
|
+
spec["description"],
|
|
61
|
+
arguments,
|
|
62
|
+
spec["function"]["returnType"],
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
return functions
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cache_specs(specs: List[SpecificationDto]):
|
|
69
|
+
supported = []
|
|
70
|
+
for spec in specs:
|
|
71
|
+
# this needs to stay in sync with logic in parse_specs
|
|
72
|
+
if spec["type"] in SUPPORTED_TYPES:
|
|
73
|
+
supported.append(spec)
|
|
74
|
+
|
|
75
|
+
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
76
|
+
full_path = os.path.join(full_path, "poly")
|
|
77
|
+
if not os.path.exists(full_path):
|
|
78
|
+
os.makedirs(full_path)
|
|
79
|
+
|
|
80
|
+
with open(os.path.join(full_path, "specs.json"), "w") as f:
|
|
81
|
+
f.write(json.dumps(supported))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_functions_and_parse(limit_ids: List[str] | None = None):
|
|
85
|
+
specs = get_specs()
|
|
86
|
+
cache_specs(specs)
|
|
87
|
+
functions = parse_function_specs(specs, limit_ids=limit_ids)
|
|
88
|
+
return functions
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_variables() -> List[VariableSpecDto]:
|
|
92
|
+
api_key, api_url = get_api_key_and_url()
|
|
93
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
94
|
+
# TODO do some caching so this and get_functions just do 1 function call
|
|
95
|
+
url = f"{api_url}/specs"
|
|
96
|
+
resp = requests.get(url, headers=headers)
|
|
97
|
+
if resp.status_code == 200:
|
|
98
|
+
specs = resp.json()
|
|
99
|
+
return [spec for spec in specs if spec['type'] == "serverVariable"]
|
|
100
|
+
else:
|
|
101
|
+
raise NotImplementedError(resp.content)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def remove_old_library():
|
|
105
|
+
currdir = os.path.dirname(os.path.abspath(__file__))
|
|
106
|
+
path = os.path.join(currdir, "poly")
|
|
107
|
+
if os.path.exists(path):
|
|
108
|
+
shutil.rmtree(path)
|
|
109
|
+
|
|
110
|
+
path = os.path.join(currdir, "vari")
|
|
111
|
+
if os.path.exists(path):
|
|
112
|
+
shutil.rmtree(path)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def generate() -> None:
|
|
116
|
+
initialize_config()
|
|
117
|
+
|
|
118
|
+
remove_old_library()
|
|
119
|
+
|
|
120
|
+
functions = get_functions_and_parse()
|
|
121
|
+
if functions:
|
|
122
|
+
generate_functions(functions)
|
|
123
|
+
else:
|
|
124
|
+
print(
|
|
125
|
+
"No functions exist yet in this tenant! Empty library initialized. Let's add some functions!"
|
|
126
|
+
)
|
|
127
|
+
exit()
|
|
128
|
+
|
|
129
|
+
variables = get_variables()
|
|
130
|
+
if variables:
|
|
131
|
+
generate_variables(variables)
|
|
132
|
+
|
|
133
|
+
# indicator to vscode extension that this is a polyapi-python project
|
|
134
|
+
file_path = os.path.join(os.getcwd(), '.polyapi-python')
|
|
135
|
+
open(file_path, 'w').close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def clear() -> None:
|
|
140
|
+
base = os.path.dirname(os.path.abspath(__file__))
|
|
141
|
+
poly_path = os.path.join(base, "poly")
|
|
142
|
+
if os.path.exists(poly_path):
|
|
143
|
+
shutil.rmtree(poly_path)
|
|
144
|
+
|
|
145
|
+
vari_path = os.path.join(base, "vari")
|
|
146
|
+
if os.path.exists(vari_path):
|
|
147
|
+
shutil.rmtree(vari_path)
|
|
148
|
+
print("Cleared!")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_function_file(
|
|
152
|
+
function_type: str,
|
|
153
|
+
full_path: str,
|
|
154
|
+
function_name: str,
|
|
155
|
+
function_id: str,
|
|
156
|
+
function_description: str,
|
|
157
|
+
arguments: List[PropertySpecification],
|
|
158
|
+
return_type: Dict[str, Any],
|
|
159
|
+
):
|
|
160
|
+
# first lets add the import to the __init__
|
|
161
|
+
init_the_init(full_path)
|
|
162
|
+
|
|
163
|
+
if function_type == "apiFunction":
|
|
164
|
+
func_str, func_type_defs = render_api_function(
|
|
165
|
+
function_type,
|
|
166
|
+
function_name,
|
|
167
|
+
function_id,
|
|
168
|
+
function_description,
|
|
169
|
+
arguments,
|
|
170
|
+
return_type,
|
|
171
|
+
)
|
|
172
|
+
elif function_type == "serverFunction":
|
|
173
|
+
func_str, func_type_defs = render_server_function(
|
|
174
|
+
function_type,
|
|
175
|
+
function_name,
|
|
176
|
+
function_id,
|
|
177
|
+
function_description,
|
|
178
|
+
arguments,
|
|
179
|
+
return_type,
|
|
180
|
+
)
|
|
181
|
+
elif function_type == "authFunction":
|
|
182
|
+
func_str, func_type_defs = render_auth_function(
|
|
183
|
+
function_type,
|
|
184
|
+
function_name,
|
|
185
|
+
function_id,
|
|
186
|
+
function_description,
|
|
187
|
+
arguments,
|
|
188
|
+
return_type,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if func_str:
|
|
192
|
+
# add function to init
|
|
193
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
194
|
+
with open(init_path, "a") as f:
|
|
195
|
+
f.write(f"\n\nfrom . import _{function_name}\n\n{func_str}")
|
|
196
|
+
|
|
197
|
+
# add type_defs to underscore file
|
|
198
|
+
file_path = os.path.join(full_path, f"_{function_name}.py")
|
|
199
|
+
with open(file_path, "w") as f:
|
|
200
|
+
f.write(func_type_defs)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_function(
|
|
204
|
+
function_type: str,
|
|
205
|
+
path: str,
|
|
206
|
+
function_id: str,
|
|
207
|
+
function_description: str,
|
|
208
|
+
arguments: List[PropertySpecification],
|
|
209
|
+
return_type: Dict[str, Any],
|
|
210
|
+
) -> None:
|
|
211
|
+
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
212
|
+
|
|
213
|
+
folders = path.split(".")
|
|
214
|
+
for idx, folder in enumerate(folders):
|
|
215
|
+
if idx + 1 == len(folders):
|
|
216
|
+
# special handling for final level
|
|
217
|
+
add_function_file(
|
|
218
|
+
function_type,
|
|
219
|
+
full_path,
|
|
220
|
+
folder,
|
|
221
|
+
function_id,
|
|
222
|
+
function_description,
|
|
223
|
+
arguments,
|
|
224
|
+
return_type,
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
full_path = os.path.join(full_path, folder)
|
|
228
|
+
if not os.path.exists(full_path):
|
|
229
|
+
os.makedirs(full_path)
|
|
230
|
+
|
|
231
|
+
# append to __init__.py file if nested folders
|
|
232
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
233
|
+
if next:
|
|
234
|
+
init_the_init(full_path)
|
|
235
|
+
add_import_to_init(full_path, next)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# TODO create the socket and pass to create_function?
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def generate_functions(functions: List) -> None:
|
|
242
|
+
for func in functions:
|
|
243
|
+
create_function(*func)
|