kaia-foundation 3.7.3__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.
- kaia_foundation-3.7.3/PKG-INFO +22 -0
- kaia_foundation-3.7.3/README.md +0 -0
- kaia_foundation-3.7.3/foundation_kaia/__init__.py +0 -0
- kaia_foundation-3.7.3/foundation_kaia/fork/__init__.py +1 -0
- kaia_foundation-3.7.3/foundation_kaia/fork/fork.py +56 -0
- kaia_foundation-3.7.3/foundation_kaia/fork/fork_worker.py +26 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/__init__.py +6 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/api.py +41 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/api_binding.py +35 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/api_utils.py +36 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/endpoint.py +26 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/format.py +67 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/marshalling_metadata.py +38 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/server.py +43 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/server_binding.py +51 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/signature_processor.py +57 -0
- kaia_foundation-3.7.3/foundation_kaia/marshalling/test_api.py +28 -0
- kaia_foundation-3.7.3/foundation_kaia/misc/__init__.py +1 -0
- kaia_foundation-3.7.3/foundation_kaia/misc/loc.py +103 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/__init__.py +7 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/abstract_prompter.py +9 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/address.py +100 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/address_builder.py +72 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/jinja_prompter.py +42 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/prompter.py +44 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/referrer.py +17 -0
- kaia_foundation-3.7.3/foundation_kaia/prompters/template_parts.py +67 -0
- kaia_foundation-3.7.3/kaia_foundation.egg-info/PKG-INFO +22 -0
- kaia_foundation-3.7.3/kaia_foundation.egg-info/SOURCES.txt +32 -0
- kaia_foundation-3.7.3/kaia_foundation.egg-info/dependency_links.txt +1 -0
- kaia_foundation-3.7.3/kaia_foundation.egg-info/requires.txt +5 -0
- kaia_foundation-3.7.3/kaia_foundation.egg-info/top_level.txt +2 -0
- kaia_foundation-3.7.3/pyproject.toml +39 -0
- kaia_foundation-3.7.3/setup.cfg +4 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaia-foundation
|
|
3
|
+
Version: 3.7.3
|
|
4
|
+
Summary: Utilities used in Kaia project
|
|
5
|
+
Author-email: Yuri Okulovsky <yuri.okulovsky@gmail.com>
|
|
6
|
+
License-Expression: LGPL-3.0-or-later
|
|
7
|
+
Project-URL: repository, https://github.com/okulovsky/kaia/tree/main/foundation_kaia
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Requires-Python: <4.0,>=3.10
|
|
18
|
+
Requires-Dist: yo_fluq
|
|
19
|
+
Requires-Dist: requests
|
|
20
|
+
Requires-Dist: jsonpickle
|
|
21
|
+
Requires-Dist: Jinja2
|
|
22
|
+
Requires-Dist: flask
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .fork import Fork
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import pickle
|
|
5
|
+
import atexit
|
|
6
|
+
import time
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from ..misc import Loc
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Fork:
|
|
13
|
+
def __init__(self, method: Callable, raise_if_exited: bool = True):
|
|
14
|
+
self.method = method
|
|
15
|
+
self.process: subprocess.Popen|None = None
|
|
16
|
+
self.monitor_thread = None
|
|
17
|
+
self.exception_raised = False
|
|
18
|
+
self.raise_if_exited = raise_if_exited
|
|
19
|
+
|
|
20
|
+
def start(self):
|
|
21
|
+
try:
|
|
22
|
+
path = Loc.temp_folder / ('fork_' + str(uuid4()))
|
|
23
|
+
with open(path, 'wb') as stream:
|
|
24
|
+
pickle.dump(self.method, stream)
|
|
25
|
+
except Exception as ex:
|
|
26
|
+
raise ValueError(f"Cannot pickle {self.method} to run in fork") from ex
|
|
27
|
+
self.process = subprocess.Popen(
|
|
28
|
+
[
|
|
29
|
+
sys.executable,
|
|
30
|
+
'-m',
|
|
31
|
+
'foundation_kaia.fork.fork_worker',
|
|
32
|
+
str(self.method),
|
|
33
|
+
str(path)
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
atexit.register(self.terminate)
|
|
37
|
+
|
|
38
|
+
if self.raise_if_exited:
|
|
39
|
+
self.monitor_thread = threading.Thread(target=self._monitor_process, daemon=True)
|
|
40
|
+
self.monitor_thread.start()
|
|
41
|
+
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def _monitor_process(self):
|
|
45
|
+
while True:
|
|
46
|
+
if self.process.poll() is not None:
|
|
47
|
+
if not self.exception_raised:
|
|
48
|
+
raise RuntimeError(f"Subprocess for {str(self.method)} exited unexpectedly with code {self.process.returncode}")
|
|
49
|
+
break
|
|
50
|
+
time.sleep(0.1)
|
|
51
|
+
|
|
52
|
+
def terminate(self):
|
|
53
|
+
if self.process and self.process.poll() is None:
|
|
54
|
+
self.exception_raised = True
|
|
55
|
+
self.process.terminate()
|
|
56
|
+
self.process.wait()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import pickle
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
if __name__ == '__main__':
|
|
7
|
+
name = sys.argv[1]
|
|
8
|
+
file = sys.argv[2]
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
with open(file, 'rb') as stream:
|
|
12
|
+
method = pickle.load(stream)
|
|
13
|
+
os.unlink(file)
|
|
14
|
+
except:
|
|
15
|
+
print(f"Subprocess for {name}: cannot read entry point", file=sys.stderr)
|
|
16
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
17
|
+
raise
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
method()
|
|
21
|
+
except:
|
|
22
|
+
print(f"Subprocess for {name}: exception in entry point", file=sys.stderr)
|
|
23
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import *
|
|
2
|
+
from .marshalling_metadata import MarshallingMetadata
|
|
3
|
+
from .api_binding import ApiBinding
|
|
4
|
+
from .api_utils import ApiUtils
|
|
5
|
+
|
|
6
|
+
def bind_to_api(api_type: Type):
|
|
7
|
+
def decorator(cls):
|
|
8
|
+
metadata = MarshallingMetadata.get_endpoints_from_type(api_type)
|
|
9
|
+
abs_methods = set(cls.__abstractmethods__)
|
|
10
|
+
for meta in metadata:
|
|
11
|
+
if meta.name not in abs_methods:
|
|
12
|
+
raise ValueError(f"{meta.name} is present in API but absent/not marked as abstract in {api_type}")
|
|
13
|
+
abs_methods.remove(meta.name)
|
|
14
|
+
cls.__abstractmethods__ = frozenset(abs_methods)
|
|
15
|
+
cls.__api_endpoints__ = metadata
|
|
16
|
+
return cls
|
|
17
|
+
return decorator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Api:
|
|
21
|
+
def __init__(self, address: str):
|
|
22
|
+
ApiUtils.check_address(address)
|
|
23
|
+
self.address = address
|
|
24
|
+
|
|
25
|
+
for meta in type(self).__api_endpoints__:
|
|
26
|
+
setattr(self, meta.name, ApiBinding(address, meta))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def wait(self, max_time_in_seconds=10):
|
|
30
|
+
ApiUtils.wait_for_reply(
|
|
31
|
+
f'http://{self.address}/heartbeat',
|
|
32
|
+
max_time_in_seconds,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from .marshalling_metadata import MarshallingMetadata
|
|
2
|
+
from .format import Format
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
class ApiBinding:
|
|
6
|
+
def __init__(self,
|
|
7
|
+
address: str,
|
|
8
|
+
metadata: MarshallingMetadata,
|
|
9
|
+
):
|
|
10
|
+
self.metadata = metadata
|
|
11
|
+
self.address = address
|
|
12
|
+
|
|
13
|
+
def __call__(self, *args, **kwargs):
|
|
14
|
+
address = f'http://{self.address}{self.metadata.get_endpoint_address()}'
|
|
15
|
+
arguments = self.metadata.signature.to_kwargs_only(*args, **kwargs)
|
|
16
|
+
data = dict(
|
|
17
|
+
arguments = Format.encode(arguments),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
reply = requests.request(
|
|
21
|
+
self.metadata.endpoint.method,
|
|
22
|
+
address,
|
|
23
|
+
json = data,
|
|
24
|
+
)
|
|
25
|
+
if reply.status_code == 200:
|
|
26
|
+
result = Format.decode(reply.json())
|
|
27
|
+
return result['result']
|
|
28
|
+
try:
|
|
29
|
+
error = reply.json()
|
|
30
|
+
except:
|
|
31
|
+
raise ValueError(f"Call to {address} caused unprocessed error on the server\n\n{reply.text}")
|
|
32
|
+
if 'error' not in error:
|
|
33
|
+
raise ValueError(f"Call to {address} caused unprocessed error on the server\n\n{error}")
|
|
34
|
+
raise ValueError(f"Call to {address} caused exception:\n\n{error['error']}")
|
|
35
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
class ApiUtils:
|
|
7
|
+
@staticmethod
|
|
8
|
+
def check_address(address):
|
|
9
|
+
if not re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}', address):
|
|
10
|
+
raise ValueError(f'Address must be IP:port, no protocol, no trailing slash, but was:\n{address}')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def wait_for_reply(url, time_in_seconds, endpoint_name=''):
|
|
15
|
+
reply = None
|
|
16
|
+
if endpoint_name == '':
|
|
17
|
+
endpoint_name = url
|
|
18
|
+
begin = datetime.datetime.now()
|
|
19
|
+
for i in range(time_in_seconds * 100):
|
|
20
|
+
time.sleep(0.01)
|
|
21
|
+
if i>2 and (datetime.datetime.now()-begin).total_seconds() > time_in_seconds:
|
|
22
|
+
break
|
|
23
|
+
try:
|
|
24
|
+
reply = requests.get(url)
|
|
25
|
+
except:
|
|
26
|
+
continue
|
|
27
|
+
if reply.status_code == 200:
|
|
28
|
+
break
|
|
29
|
+
if reply is None:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Endpoint {endpoint_name} was not reacheable within {time_in_seconds} seconds")
|
|
32
|
+
if reply.status_code != 200:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Endpoint {endpoint_name} was reacheable, but returned bad status code {reply.status_code}\n{reply.text}")
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class EndpointMetadata:
|
|
7
|
+
url: str|None
|
|
8
|
+
method: str
|
|
9
|
+
json_pickle_result: bool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def endpoint(
|
|
13
|
+
*,
|
|
14
|
+
url: str|None = None,
|
|
15
|
+
method = 'POST',
|
|
16
|
+
json_pickle_result: bool = False
|
|
17
|
+
):
|
|
18
|
+
def decorator(func):
|
|
19
|
+
# Add the metadata as an attribute of the function
|
|
20
|
+
func._metadata = EndpointMetadata(url, method, json_pickle_result)
|
|
21
|
+
|
|
22
|
+
@wraps(func)
|
|
23
|
+
def wrapper(*args, **kwargs):
|
|
24
|
+
return func(*args, **kwargs)
|
|
25
|
+
return wrapper
|
|
26
|
+
return decorator
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pickle
|
|
3
|
+
import base64
|
|
4
|
+
import jsonpickle
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NoTupleJSONEncoder(json.JSONEncoder):
|
|
9
|
+
def default(self, obj):
|
|
10
|
+
if isinstance(obj, tuple):
|
|
11
|
+
raise TypeError("Tuples are not allowed in JSON serialization.")
|
|
12
|
+
return super().default(obj)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Format:
|
|
16
|
+
CONTROL_FIELD = '@type'
|
|
17
|
+
CONTENT_FIELD = '@content'
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def check_json(obj):
|
|
21
|
+
if obj is None:
|
|
22
|
+
return True
|
|
23
|
+
if isinstance(obj, str) or isinstance(obj, int) or isinstance(obj, float) or isinstance(obj, bool):
|
|
24
|
+
return True
|
|
25
|
+
if isinstance(obj, list):
|
|
26
|
+
return all(Format.check_json(z) for z in obj)
|
|
27
|
+
if isinstance(obj, dict):
|
|
28
|
+
return all(Format.check_json(z) for z in obj.values())
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def encode(data: dict, use_json_pickle: bool = False):
|
|
33
|
+
result = {}
|
|
34
|
+
for key, value in data.items():
|
|
35
|
+
if Format.check_json(value):
|
|
36
|
+
result[key] = value
|
|
37
|
+
else:
|
|
38
|
+
if use_json_pickle:
|
|
39
|
+
result[key] = {
|
|
40
|
+
Format.CONTROL_FIELD: 'jsonpickle',
|
|
41
|
+
Format.CONTENT_FIELD: json.loads(jsonpickle.dumps(value))
|
|
42
|
+
}
|
|
43
|
+
else:
|
|
44
|
+
s = base64.b64encode(pickle.dumps(value)).decode('ascii')
|
|
45
|
+
result[key] = {
|
|
46
|
+
Format.CONTROL_FIELD: 'base64', Format.CONTENT_FIELD: s
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def decode(data: dict):
|
|
52
|
+
result = {}
|
|
53
|
+
for key in data:
|
|
54
|
+
if (
|
|
55
|
+
isinstance(data[key], dict) and
|
|
56
|
+
Format.CONTROL_FIELD in data[key] and
|
|
57
|
+
Format.CONTENT_FIELD in data[key]
|
|
58
|
+
):
|
|
59
|
+
if data[key][Format.CONTROL_FIELD] == 'base64':
|
|
60
|
+
result[key] = pickle.loads(base64.b64decode(data[key][Format.CONTENT_FIELD]))
|
|
61
|
+
elif data[key][Format.CONTROL_FIELD] == 'jsonpickle':
|
|
62
|
+
result[key] = jsonpickle.loads(json.dumps(data[key][Format.CONTENT_FIELD]))
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("only @type=base64 and @type=jsonpickle is supported")
|
|
65
|
+
else:
|
|
66
|
+
result[key] = data[key]
|
|
67
|
+
return result
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import *
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from .signature_processor import SignatureProcessor
|
|
4
|
+
from .endpoint import EndpointMetadata
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class MarshallingMetadata:
|
|
8
|
+
name: str
|
|
9
|
+
method: Callable
|
|
10
|
+
endpoint: EndpointMetadata
|
|
11
|
+
signature: SignatureProcessor
|
|
12
|
+
|
|
13
|
+
def get_endpoint_address(self):
|
|
14
|
+
if self.endpoint.url is None:
|
|
15
|
+
return '/'+self.name
|
|
16
|
+
return self.endpoint.url
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _get_endpoints(_obj, _type) -> tuple['MarshallingMetadata',...]:
|
|
20
|
+
result = []
|
|
21
|
+
for attr_name, attr_value in _type.__dict__.items():
|
|
22
|
+
if not hasattr(attr_value,'_metadata'):
|
|
23
|
+
continue
|
|
24
|
+
result.append(MarshallingMetadata(
|
|
25
|
+
attr_name,
|
|
26
|
+
getattr(_obj, attr_name) if _obj is not None else None,
|
|
27
|
+
attr_value._metadata,
|
|
28
|
+
SignatureProcessor.from_signature(attr_value)
|
|
29
|
+
))
|
|
30
|
+
return tuple(result)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_endpoints_from_object(obj) -> tuple['MarshallingMetadata',...]:
|
|
34
|
+
return MarshallingMetadata._get_endpoints(obj, type(obj))
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def get_endpoints_from_type(type: Type) -> tuple['MarshallingMetadata',...]:
|
|
38
|
+
return MarshallingMetadata._get_endpoints(None, type)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .marshalling_metadata import MarshallingMetadata
|
|
2
|
+
from flask import Flask
|
|
3
|
+
from .server_binding import ServerBinding
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Server:
|
|
7
|
+
def __init__(self, port: int, *objects):
|
|
8
|
+
self.objects = objects
|
|
9
|
+
self.port = port
|
|
10
|
+
metadata = []
|
|
11
|
+
for object in objects:
|
|
12
|
+
metadata.extend(MarshallingMetadata.get_endpoints_from_object(object))
|
|
13
|
+
self.metadata = tuple(metadata)
|
|
14
|
+
|
|
15
|
+
def create_alternative_binding(self, name: str, address: str):
|
|
16
|
+
meta = [m for m in self.metadata if m.get_endpoint_address() == address]
|
|
17
|
+
if len(meta) != 1:
|
|
18
|
+
raise ValueError(f"Too much/none ({len(meta)} endpoints for address {address}")
|
|
19
|
+
return ServerBinding(meta[0], name)
|
|
20
|
+
|
|
21
|
+
def bind_endpoints(self, app: Flask):
|
|
22
|
+
for metadata in self.metadata:
|
|
23
|
+
app.add_url_rule(
|
|
24
|
+
metadata.get_endpoint_address(),
|
|
25
|
+
view_func=ServerBinding(metadata),
|
|
26
|
+
methods=[metadata.endpoint.method]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def bind_heartbeat(self, app: Flask):
|
|
30
|
+
app.add_url_rule('/heartbeat', view_func=self.heartbeat, methods=['GET'])
|
|
31
|
+
|
|
32
|
+
def bind_app(self, app: Flask):
|
|
33
|
+
self.bind_endpoints(app)
|
|
34
|
+
self.bind_heartbeat(app)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def heartbeat(self):
|
|
38
|
+
return 'OK'
|
|
39
|
+
|
|
40
|
+
def __call__(self):
|
|
41
|
+
app = Flask('RPC_'+'_'.join(type(o).__name__ for o in self.objects))
|
|
42
|
+
self.bind_app(app)
|
|
43
|
+
app.run('0.0.0.0', self.port)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from .marshalling_metadata import MarshallingMetadata
|
|
2
|
+
from .format import Format
|
|
3
|
+
import flask
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ServerBinding:
|
|
8
|
+
def __init__(self,
|
|
9
|
+
meta :MarshallingMetadata,
|
|
10
|
+
custom_method_name: str|None = None
|
|
11
|
+
):
|
|
12
|
+
self.meta = meta
|
|
13
|
+
self.custom_method_name = custom_method_name
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def __name__(self):
|
|
17
|
+
if self.custom_method_name is None:
|
|
18
|
+
return self.meta.name
|
|
19
|
+
return self.custom_method_name
|
|
20
|
+
|
|
21
|
+
def _get_arguments(self, kwargs, data):
|
|
22
|
+
arguments = Format.decode(data['arguments'])
|
|
23
|
+
for key, value in kwargs.items():
|
|
24
|
+
if key in arguments:
|
|
25
|
+
raise ValueError(f"{key} is provided via address and via json")
|
|
26
|
+
arguments[key] = value
|
|
27
|
+
return arguments
|
|
28
|
+
|
|
29
|
+
def _process(self, kwargs, arguments):
|
|
30
|
+
arguments = self._get_arguments(kwargs, arguments)
|
|
31
|
+
result = self.meta.method(**arguments)
|
|
32
|
+
result = dict(result=result, error = None)
|
|
33
|
+
result = Format.encode(result, self.meta.endpoint.json_pickle_result)
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
def __call__(self, **kwargs):
|
|
37
|
+
data = flask.request.json
|
|
38
|
+
try:
|
|
39
|
+
return flask.jsonify(self._process(kwargs, data))
|
|
40
|
+
except:
|
|
41
|
+
tb = traceback.format_exc()
|
|
42
|
+
print(tb)
|
|
43
|
+
return flask.jsonify(dict(result=None, error=tb)), 500
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SignatureProcessor:
|
|
7
|
+
mandatory: tuple[str,...]
|
|
8
|
+
optional: tuple[str,...]
|
|
9
|
+
open: bool
|
|
10
|
+
|
|
11
|
+
def to_kwargs_only(self, *args, **kwargs):
|
|
12
|
+
all = self.mandatory + self.optional
|
|
13
|
+
if len(args) > len(all):
|
|
14
|
+
raise ValueError(f"args has {len(args)} arguments, while there are only {len(all)} arguments")
|
|
15
|
+
result = OrderedDict()
|
|
16
|
+
for i, arg in enumerate(args):
|
|
17
|
+
name = all[i]
|
|
18
|
+
if name in kwargs:
|
|
19
|
+
raise ValueError(f"Args at index {i} conflicts with kwargs {name}")
|
|
20
|
+
result[name] = arg
|
|
21
|
+
|
|
22
|
+
for key, value in kwargs.items():
|
|
23
|
+
result[key] = value
|
|
24
|
+
|
|
25
|
+
mandatory_seen = set()
|
|
26
|
+
for key in result:
|
|
27
|
+
if key in self.mandatory:
|
|
28
|
+
mandatory_seen.add(key)
|
|
29
|
+
elif key in self.optional:
|
|
30
|
+
pass
|
|
31
|
+
else:
|
|
32
|
+
if not open:
|
|
33
|
+
raise ValueError(f"Argument `{key}` is not in fields, and the signature is not open")
|
|
34
|
+
|
|
35
|
+
if len(mandatory_seen) < len(self.mandatory):
|
|
36
|
+
not_seen = ", ".join([f'`{c}`' for c in self.mandatory if c not in mandatory_seen])
|
|
37
|
+
raise TypeError(f"Not all mandatory arguments are provided: missing {not_seen}")
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def from_signature(method):
|
|
43
|
+
signature = inspect.signature(method)
|
|
44
|
+
mandatory = list()
|
|
45
|
+
optional = list()
|
|
46
|
+
open = False
|
|
47
|
+
for i,(p,v) in enumerate(signature.parameters.items()):
|
|
48
|
+
if i == 0 and v.name=='self':
|
|
49
|
+
continue
|
|
50
|
+
if v.kind == inspect.Parameter.VAR_KEYWORD:
|
|
51
|
+
open = True
|
|
52
|
+
else:
|
|
53
|
+
if v.default == inspect._empty:
|
|
54
|
+
mandatory.append(v.name)
|
|
55
|
+
else:
|
|
56
|
+
optional.append(v.name)
|
|
57
|
+
return SignatureProcessor(tuple(mandatory), tuple(optional), open)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import TypeVar, Generic, Callable
|
|
2
|
+
from .server import Server
|
|
3
|
+
from .api import Api
|
|
4
|
+
from ..fork import Fork
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
TApi = TypeVar('TApi')
|
|
8
|
+
|
|
9
|
+
class TestApi(Generic[TApi]):
|
|
10
|
+
def __init__(self,
|
|
11
|
+
api_factory: Callable[[str], Api],
|
|
12
|
+
server: Server
|
|
13
|
+
):
|
|
14
|
+
self.api_factory = api_factory
|
|
15
|
+
self.server = server
|
|
16
|
+
self.fork = None
|
|
17
|
+
|
|
18
|
+
def __enter__(self) -> TApi:
|
|
19
|
+
self.fork = Fork(self.server).start()
|
|
20
|
+
api = self.api_factory(f'127.0.0.1:{self.server.port}')
|
|
21
|
+
api.wait()
|
|
22
|
+
return api
|
|
23
|
+
|
|
24
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
25
|
+
if self.fork is not None:
|
|
26
|
+
self.fork.terminate()
|
|
27
|
+
|
|
28
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .loc import Loc, Locator
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shutil
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
class TempFile:
|
|
8
|
+
def __init__(self, path: Path, dont_delete: bool):
|
|
9
|
+
self.path = path
|
|
10
|
+
self.dont_delete = dont_delete
|
|
11
|
+
|
|
12
|
+
def __enter__(self):
|
|
13
|
+
os.makedirs(self.path.parent, exist_ok=True)
|
|
14
|
+
if self.path.is_file():
|
|
15
|
+
os.unlink(self.path)
|
|
16
|
+
return self.path
|
|
17
|
+
|
|
18
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
19
|
+
if not self.dont_delete:
|
|
20
|
+
if self.path.is_file():
|
|
21
|
+
try:
|
|
22
|
+
os.unlink(self.path)
|
|
23
|
+
except:
|
|
24
|
+
print("Cannot delete test file:\n"+traceback.format_exc())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TempFolder:
|
|
28
|
+
def __init__(self, path: Path, dont_delete: bool = False):
|
|
29
|
+
self.path = path
|
|
30
|
+
self.dont_delete = dont_delete
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
if self.path.is_dir():
|
|
34
|
+
shutil.rmtree(self.path)
|
|
35
|
+
os.makedirs(self.path)
|
|
36
|
+
return self.path
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
39
|
+
if not self.dont_delete and self.path.is_dir():
|
|
40
|
+
shutil.rmtree(self.path, ignore_errors=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Locator:
|
|
44
|
+
def __init__(self, root_path: Path|None = None):
|
|
45
|
+
if root_path is None:
|
|
46
|
+
root_path = Path(__file__).parent.parent.parent
|
|
47
|
+
self._root_path = root_path
|
|
48
|
+
|
|
49
|
+
def _make_and_return(self, path) -> Path:
|
|
50
|
+
os.makedirs(path, exist_ok=True)
|
|
51
|
+
return path
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def data_folder(self) -> Path:
|
|
55
|
+
return self._make_and_return(self._root_path/'data')
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def resources_folder(self) -> Path:
|
|
59
|
+
return self._make_and_return(self._root_path/'data/resources')
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cache_folder(self) -> Path:
|
|
63
|
+
return self._make_and_return(self._root_path/'temp/brainbox_cache')
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def temp_folder(self) -> Path:
|
|
67
|
+
return self._make_and_return(self._root_path/'temp')
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def test_folder(self) -> Path:
|
|
71
|
+
return self._make_and_return(self._root_path/'temp/tests')
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def self_test_path(self) -> Path:
|
|
75
|
+
return self._make_and_return(self._root_path/'data/brainbox_self_test')
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def db_path(self) -> Path:
|
|
79
|
+
return self.data_folder/'brainbox.db'
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def root_folder(self) -> Path:
|
|
83
|
+
return self._root_path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_test_file(self, extension_without_leading_dot: str|None = None, subfolder: str|None = None, dont_delete: bool = False) -> TempFile:
|
|
87
|
+
path = self.test_folder
|
|
88
|
+
if subfolder is not None:
|
|
89
|
+
path /= subfolder
|
|
90
|
+
name = str(uuid4())
|
|
91
|
+
if extension_without_leading_dot is not None:
|
|
92
|
+
name+= '.'+extension_without_leading_dot
|
|
93
|
+
path/=name
|
|
94
|
+
return TempFile(path, dont_delete)
|
|
95
|
+
|
|
96
|
+
def create_test_folder(self, subfolder: str|None = None, dont_delete: bool = False) -> TempFolder:
|
|
97
|
+
path = self.test_folder
|
|
98
|
+
if subfolder is not None:
|
|
99
|
+
path /= subfolder
|
|
100
|
+
path /= str(uuid4())
|
|
101
|
+
return TempFolder(path, dont_delete)
|
|
102
|
+
|
|
103
|
+
Loc = Locator()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .abstract_prompter import IPrompter
|
|
2
|
+
from .address import Address, IAddressElement, DefaultElement
|
|
3
|
+
from .address_builder import AddressBuilder, AddressBuilderGC
|
|
4
|
+
from .prompter import Prompter
|
|
5
|
+
from .template_parts import ITemplatePart, ConstantTemplatePart, AddressTemplatePart, SubpromptPropagationTemplatePart
|
|
6
|
+
from .referrer import Referrer
|
|
7
|
+
from .jinja_prompter import JinjaPrompter
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class IAddressElement(ABC):
|
|
4
|
+
@abstractmethod
|
|
5
|
+
def get(self, obj):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def set(self, obj, value):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def translate(element) -> 'IAddressElement':
|
|
14
|
+
if isinstance(element, IAddressElement):
|
|
15
|
+
return element
|
|
16
|
+
return DefaultElement(element)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DefaultElement(IAddressElement):
|
|
21
|
+
def __init__(self, element):
|
|
22
|
+
self.element = element
|
|
23
|
+
|
|
24
|
+
def get(self, obj):
|
|
25
|
+
if isinstance(self.element, str) and hasattr(obj, self.element):
|
|
26
|
+
return getattr(obj, self.element)
|
|
27
|
+
if hasattr(obj, '__getitem__'):
|
|
28
|
+
getitem = getattr(obj, '__getitem__')
|
|
29
|
+
if callable(getitem):
|
|
30
|
+
return getitem(self.element)
|
|
31
|
+
raise ValueError(f"Obj {obj} cannot be addressed with {self.element}")
|
|
32
|
+
|
|
33
|
+
def set(self, obj, value):
|
|
34
|
+
if hasattr(obj, '__setitem__'):
|
|
35
|
+
setitem = getattr(obj, '__setitem__')
|
|
36
|
+
if callable(setitem):
|
|
37
|
+
setitem(obj, self.element, value)
|
|
38
|
+
return
|
|
39
|
+
if isinstance(self.element, str):
|
|
40
|
+
setattr(obj, self.element, value)
|
|
41
|
+
return
|
|
42
|
+
raise ValueError(f"Obj {obj} cannot be set with address `{self.element}`")
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
if isinstance(self.element, str):
|
|
46
|
+
return self.element
|
|
47
|
+
return f'[{self.element}]'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Address:
|
|
51
|
+
def __init__(self, *address):
|
|
52
|
+
from .address_builder import AddressBuilder
|
|
53
|
+
if len(address) == 1:
|
|
54
|
+
if isinstance(address[0], AddressBuilder):
|
|
55
|
+
self.address = address[0]._address_builder_stored_address.address
|
|
56
|
+
elif isinstance(address[0], Address):
|
|
57
|
+
self.address = address[0].address
|
|
58
|
+
else:
|
|
59
|
+
self.address = (IAddressElement.translate(address[0]), )
|
|
60
|
+
else:
|
|
61
|
+
self.address: tuple[IAddressElement,...] = tuple(IAddressElement.translate(a) for a in address)
|
|
62
|
+
|
|
63
|
+
def get(self, obj):
|
|
64
|
+
result = obj
|
|
65
|
+
for index, element in enumerate(self.address):
|
|
66
|
+
try:
|
|
67
|
+
result = element.get(result)
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
raise ValueError(f"Address {self} failed at {index} for object\n{obj}") from exc
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def set(self, obj, value):
|
|
73
|
+
obj = self.pop().get(obj)
|
|
74
|
+
self.address[-1].set(obj, value)
|
|
75
|
+
|
|
76
|
+
def append(self, value: str|IAddressElement):
|
|
77
|
+
if isinstance(value, str):
|
|
78
|
+
value= IAddressElement.translate(value)
|
|
79
|
+
elif isinstance(value, IAddressElement):
|
|
80
|
+
pass
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError("Expected string or IAddressElement")
|
|
83
|
+
return Address(*(self.address+(value,)))
|
|
84
|
+
|
|
85
|
+
def pop(self) -> 'Address':
|
|
86
|
+
if len(self.address) == 0:
|
|
87
|
+
raise ValueError("Can't pop from empty address")
|
|
88
|
+
return Address(*self.address[:-1])
|
|
89
|
+
|
|
90
|
+
def is_empty(self):
|
|
91
|
+
return len(self.address) == 0
|
|
92
|
+
|
|
93
|
+
def __str__(self):
|
|
94
|
+
return '.'.join(str(a) for a in self.address)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def parse(s: str):
|
|
98
|
+
parts = s.split('.')
|
|
99
|
+
parts = [DefaultElement(e) for e in parts]
|
|
100
|
+
return Address(*parts)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import *
|
|
2
|
+
from .address import DefaultElement, Address
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AddressBuilderGC:
|
|
8
|
+
class Dimension(IntEnum):
|
|
9
|
+
address = 0
|
|
10
|
+
operator = 1
|
|
11
|
+
subprompt = 2
|
|
12
|
+
misc = 100
|
|
13
|
+
|
|
14
|
+
cache: dict['AddressBuilderGC.Dimension', dict[str,Any]] = {}
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def record(dimension: 'AddressBuilderGC.Dimension', id: str, value: Any):
|
|
18
|
+
if dimension not in AddressBuilderGC.cache:
|
|
19
|
+
AddressBuilderGC.cache[dimension] = {}
|
|
20
|
+
AddressBuilderGC.cache[dimension][id] = value
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def find(dimension: 'AddressBuilderGC.Dimension', id: str):
|
|
24
|
+
if dimension not in AddressBuilderGC.cache:
|
|
25
|
+
return None
|
|
26
|
+
if id not in AddressBuilderGC.cache[dimension]:
|
|
27
|
+
return None
|
|
28
|
+
return AddressBuilderGC.cache[dimension][id]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
RESERVED_FIELDS = {
|
|
32
|
+
'_address_builder_stored_address',
|
|
33
|
+
'_address_builder_uuid',
|
|
34
|
+
'__str__',
|
|
35
|
+
'__truediv__',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class AddressBuilder:
|
|
39
|
+
def __init__(self, address: Address|None = None):
|
|
40
|
+
self._address_builder_uuid = 'id'+str(uuid4()).replace('-','')
|
|
41
|
+
self._address_builder_stored_address = address if address is not None else Address()
|
|
42
|
+
AddressBuilderGC.record(
|
|
43
|
+
AddressBuilderGC.Dimension.address,
|
|
44
|
+
self._address_builder_uuid,
|
|
45
|
+
self._address_builder_stored_address
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return f'<<{self._address_builder_uuid}>>'
|
|
50
|
+
|
|
51
|
+
def __getattribute__(self, item):
|
|
52
|
+
if item in RESERVED_FIELDS:
|
|
53
|
+
return super().__getattribute__(item)
|
|
54
|
+
return AddressBuilder(self._address_builder_stored_address.append(DefaultElement(item)))
|
|
55
|
+
|
|
56
|
+
def __truediv__(self, other):
|
|
57
|
+
AddressBuilderGC.record(
|
|
58
|
+
AddressBuilderGC.Dimension.operator,
|
|
59
|
+
self._address_builder_uuid,
|
|
60
|
+
other
|
|
61
|
+
)
|
|
62
|
+
return self.__str__()
|
|
63
|
+
|
|
64
|
+
def __invert__(self):
|
|
65
|
+
AddressBuilderGC.record(
|
|
66
|
+
AddressBuilderGC.Dimension.subprompt,
|
|
67
|
+
self._address_builder_uuid,
|
|
68
|
+
True
|
|
69
|
+
)
|
|
70
|
+
return self.__str__()
|
|
71
|
+
|
|
72
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from jinja2 import Template, Environment, meta
|
|
2
|
+
from typing import Generic
|
|
3
|
+
from .abstract_prompter import IPrompter, T
|
|
4
|
+
from dataclasses import is_dataclass
|
|
5
|
+
from copy import copy
|
|
6
|
+
import re
|
|
7
|
+
class JinjaPrompter(IPrompter, Generic[T]):
|
|
8
|
+
def __init__(self, template: str, prettify_newlines: bool = True):
|
|
9
|
+
self._template_text = template.replace('<<', '{{').replace('>>', '}}')
|
|
10
|
+
self._template = Template(self._template_text)
|
|
11
|
+
self._prettify_newlines = prettify_newlines
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def normalize_newlines(text: str) -> str:
|
|
15
|
+
text = re.sub(r'\n{2,}', '\n\n', text)
|
|
16
|
+
text = re.sub(r'(?<!\n)\n(?!\n)', ' ', text)
|
|
17
|
+
return text
|
|
18
|
+
|
|
19
|
+
def get_variables(self):
|
|
20
|
+
env = Environment()
|
|
21
|
+
parsed_content = env.parse(self._template_text)
|
|
22
|
+
variables = meta.find_undeclared_variables(parsed_content)
|
|
23
|
+
return variables
|
|
24
|
+
|
|
25
|
+
def __call__(self, obj: T) -> str:
|
|
26
|
+
values = {}
|
|
27
|
+
if is_dataclass(obj):
|
|
28
|
+
if isinstance(obj, type):
|
|
29
|
+
pass
|
|
30
|
+
else:
|
|
31
|
+
values = copy(obj.__dict__)
|
|
32
|
+
elif isinstance(obj, dict):
|
|
33
|
+
values = copy(obj)
|
|
34
|
+
values['_'] = obj
|
|
35
|
+
s = self._template.render(**values)
|
|
36
|
+
|
|
37
|
+
if self._prettify_newlines:
|
|
38
|
+
s = JinjaPrompter.normalize_newlines(s)
|
|
39
|
+
|
|
40
|
+
return s
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .abstract_prompter import IPrompter, T, Generic
|
|
2
|
+
from .address import Address
|
|
3
|
+
from .template_parts import ITemplatePart, ConstantTemplatePart
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
class Parser:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.parts = []
|
|
9
|
+
|
|
10
|
+
def parse_next(self, part: str):
|
|
11
|
+
if part.startswith('<<') and part.endswith('>>'):
|
|
12
|
+
uid = part[2:-2]
|
|
13
|
+
token = ITemplatePart.parse_gc(uid)
|
|
14
|
+
self.parts.append(token)
|
|
15
|
+
else:
|
|
16
|
+
if part != '':
|
|
17
|
+
self.parts.append(ConstantTemplatePart(part))
|
|
18
|
+
|
|
19
|
+
def finalize(self):
|
|
20
|
+
return tuple(self.parts)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse(template: str) -> tuple[ITemplatePart,...]:
|
|
25
|
+
parser = Parser()
|
|
26
|
+
parts = re.split('(<<[^>]+>>)', template)
|
|
27
|
+
for p in parts:
|
|
28
|
+
parser.parse_next(p)
|
|
29
|
+
return parser.finalize()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Prompter(IPrompter[T], Generic[T]):
|
|
33
|
+
def __init__(self, template: str|tuple[ITemplatePart,...]):
|
|
34
|
+
if isinstance(template, str):
|
|
35
|
+
self.template = _parse(template)
|
|
36
|
+
else:
|
|
37
|
+
self.template = template
|
|
38
|
+
|
|
39
|
+
def __call__(self, obj: T) -> str:
|
|
40
|
+
return ''.join(part.to_str(obj) for part in self.template)
|
|
41
|
+
|
|
42
|
+
def to_readable_string(self):
|
|
43
|
+
return ''.join(p.to_readable_expression() for p in self.template)
|
|
44
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .address_builder import AddressBuilder
|
|
2
|
+
from typing import TypeVar, Generic
|
|
3
|
+
|
|
4
|
+
T = TypeVar('T')
|
|
5
|
+
|
|
6
|
+
class Referrer(Generic[T]):
|
|
7
|
+
@property
|
|
8
|
+
def ref(self) -> T:
|
|
9
|
+
return AddressBuilder()
|
|
10
|
+
|
|
11
|
+
def list_to_bullet_points(self, bullet_point = '* '):
|
|
12
|
+
def _f(value):
|
|
13
|
+
return '\n'.join(bullet_point+s for s in value)
|
|
14
|
+
return _f
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import *
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from .address_builder import AddressBuilderGC
|
|
5
|
+
from .address import Address
|
|
6
|
+
from .abstract_prompter import IPrompter
|
|
7
|
+
from .referrer import Referrer
|
|
8
|
+
|
|
9
|
+
class ITemplatePart(ABC):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def to_str(self, value: Any) -> str:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def to_readable_expression(self) -> str:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def parse_gc(uid: str):
|
|
20
|
+
address = AddressBuilderGC.find(AddressBuilderGC.Dimension.address, uid)
|
|
21
|
+
if address is None:
|
|
22
|
+
raise ValueError(f"Cannot find uid {uid} in the cache")
|
|
23
|
+
if AddressBuilderGC.find(AddressBuilderGC.Dimension.subprompt, uid):
|
|
24
|
+
return SubpromptPropagationTemplatePart(address)
|
|
25
|
+
else:
|
|
26
|
+
return AddressTemplatePart(
|
|
27
|
+
address,
|
|
28
|
+
AddressBuilderGC.find(AddressBuilderGC.Dimension.operator, uid),
|
|
29
|
+
AddressBuilderGC.find(AddressBuilderGC.Dimension.misc, uid),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ConstantTemplatePart(ITemplatePart):
|
|
35
|
+
value: str
|
|
36
|
+
|
|
37
|
+
def to_str(self, value: Any) -> str:
|
|
38
|
+
return self.value
|
|
39
|
+
|
|
40
|
+
def to_readable_expression(self) -> str:
|
|
41
|
+
return self.value
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AddressTemplatePart(ITemplatePart):
|
|
45
|
+
address: Address
|
|
46
|
+
formatter: Callable[[Any], str]|None = None
|
|
47
|
+
misc: Any = None
|
|
48
|
+
|
|
49
|
+
def to_str(self, value: Any) -> str:
|
|
50
|
+
result = self.address.get(value)
|
|
51
|
+
if self.formatter is not None:
|
|
52
|
+
result = self.formatter(result)
|
|
53
|
+
return str(result)
|
|
54
|
+
|
|
55
|
+
def to_readable_expression(self) -> str:
|
|
56
|
+
return '{{'+self.address.__str__()+'}}'
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class SubpromptPropagationTemplatePart(ITemplatePart):
|
|
60
|
+
address: Address
|
|
61
|
+
|
|
62
|
+
def to_str(self, value: Any):
|
|
63
|
+
return self.address.get(value)(value)
|
|
64
|
+
|
|
65
|
+
def to_readable_expression(self) -> str:
|
|
66
|
+
return '{{'+self.address.__str__()+"(_)}}"
|
|
67
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaia-foundation
|
|
3
|
+
Version: 3.7.3
|
|
4
|
+
Summary: Utilities used in Kaia project
|
|
5
|
+
Author-email: Yuri Okulovsky <yuri.okulovsky@gmail.com>
|
|
6
|
+
License-Expression: LGPL-3.0-or-later
|
|
7
|
+
Project-URL: repository, https://github.com/okulovsky/kaia/tree/main/foundation_kaia
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Requires-Python: <4.0,>=3.10
|
|
18
|
+
Requires-Dist: yo_fluq
|
|
19
|
+
Requires-Dist: requests
|
|
20
|
+
Requires-Dist: jsonpickle
|
|
21
|
+
Requires-Dist: Jinja2
|
|
22
|
+
Requires-Dist: flask
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
foundation_kaia/__init__.py
|
|
4
|
+
foundation_kaia/fork/__init__.py
|
|
5
|
+
foundation_kaia/fork/fork.py
|
|
6
|
+
foundation_kaia/fork/fork_worker.py
|
|
7
|
+
foundation_kaia/marshalling/__init__.py
|
|
8
|
+
foundation_kaia/marshalling/api.py
|
|
9
|
+
foundation_kaia/marshalling/api_binding.py
|
|
10
|
+
foundation_kaia/marshalling/api_utils.py
|
|
11
|
+
foundation_kaia/marshalling/endpoint.py
|
|
12
|
+
foundation_kaia/marshalling/format.py
|
|
13
|
+
foundation_kaia/marshalling/marshalling_metadata.py
|
|
14
|
+
foundation_kaia/marshalling/server.py
|
|
15
|
+
foundation_kaia/marshalling/server_binding.py
|
|
16
|
+
foundation_kaia/marshalling/signature_processor.py
|
|
17
|
+
foundation_kaia/marshalling/test_api.py
|
|
18
|
+
foundation_kaia/misc/__init__.py
|
|
19
|
+
foundation_kaia/misc/loc.py
|
|
20
|
+
foundation_kaia/prompters/__init__.py
|
|
21
|
+
foundation_kaia/prompters/abstract_prompter.py
|
|
22
|
+
foundation_kaia/prompters/address.py
|
|
23
|
+
foundation_kaia/prompters/address_builder.py
|
|
24
|
+
foundation_kaia/prompters/jinja_prompter.py
|
|
25
|
+
foundation_kaia/prompters/prompter.py
|
|
26
|
+
foundation_kaia/prompters/referrer.py
|
|
27
|
+
foundation_kaia/prompters/template_parts.py
|
|
28
|
+
kaia_foundation.egg-info/PKG-INFO
|
|
29
|
+
kaia_foundation.egg-info/SOURCES.txt
|
|
30
|
+
kaia_foundation.egg-info/dependency_links.txt
|
|
31
|
+
kaia_foundation.egg-info/requires.txt
|
|
32
|
+
kaia_foundation.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kaia-foundation"
|
|
7
|
+
description = "Utilities used in Kaia project"
|
|
8
|
+
version = "3.7.3"
|
|
9
|
+
requires-python = ">=3.10, <4.0"
|
|
10
|
+
license = "LGPL-3.0-or-later"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Yuri Okulovsky", email = "yuri.okulovsky@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"yo_fluq",
|
|
17
|
+
"requests",
|
|
18
|
+
"jsonpickle",
|
|
19
|
+
"Jinja2",
|
|
20
|
+
"flask"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Intended Audience :: Science/Research",
|
|
30
|
+
"Intended Audience :: Information Technology",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
|
+
"Development Status :: 4 - Beta"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
repository = "https://github.com/okulovsky/kaia/tree/main/foundation_kaia"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|