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.
Files changed (34) hide show
  1. kaia_foundation-3.7.3/PKG-INFO +22 -0
  2. kaia_foundation-3.7.3/README.md +0 -0
  3. kaia_foundation-3.7.3/foundation_kaia/__init__.py +0 -0
  4. kaia_foundation-3.7.3/foundation_kaia/fork/__init__.py +1 -0
  5. kaia_foundation-3.7.3/foundation_kaia/fork/fork.py +56 -0
  6. kaia_foundation-3.7.3/foundation_kaia/fork/fork_worker.py +26 -0
  7. kaia_foundation-3.7.3/foundation_kaia/marshalling/__init__.py +6 -0
  8. kaia_foundation-3.7.3/foundation_kaia/marshalling/api.py +41 -0
  9. kaia_foundation-3.7.3/foundation_kaia/marshalling/api_binding.py +35 -0
  10. kaia_foundation-3.7.3/foundation_kaia/marshalling/api_utils.py +36 -0
  11. kaia_foundation-3.7.3/foundation_kaia/marshalling/endpoint.py +26 -0
  12. kaia_foundation-3.7.3/foundation_kaia/marshalling/format.py +67 -0
  13. kaia_foundation-3.7.3/foundation_kaia/marshalling/marshalling_metadata.py +38 -0
  14. kaia_foundation-3.7.3/foundation_kaia/marshalling/server.py +43 -0
  15. kaia_foundation-3.7.3/foundation_kaia/marshalling/server_binding.py +51 -0
  16. kaia_foundation-3.7.3/foundation_kaia/marshalling/signature_processor.py +57 -0
  17. kaia_foundation-3.7.3/foundation_kaia/marshalling/test_api.py +28 -0
  18. kaia_foundation-3.7.3/foundation_kaia/misc/__init__.py +1 -0
  19. kaia_foundation-3.7.3/foundation_kaia/misc/loc.py +103 -0
  20. kaia_foundation-3.7.3/foundation_kaia/prompters/__init__.py +7 -0
  21. kaia_foundation-3.7.3/foundation_kaia/prompters/abstract_prompter.py +9 -0
  22. kaia_foundation-3.7.3/foundation_kaia/prompters/address.py +100 -0
  23. kaia_foundation-3.7.3/foundation_kaia/prompters/address_builder.py +72 -0
  24. kaia_foundation-3.7.3/foundation_kaia/prompters/jinja_prompter.py +42 -0
  25. kaia_foundation-3.7.3/foundation_kaia/prompters/prompter.py +44 -0
  26. kaia_foundation-3.7.3/foundation_kaia/prompters/referrer.py +17 -0
  27. kaia_foundation-3.7.3/foundation_kaia/prompters/template_parts.py +67 -0
  28. kaia_foundation-3.7.3/kaia_foundation.egg-info/PKG-INFO +22 -0
  29. kaia_foundation-3.7.3/kaia_foundation.egg-info/SOURCES.txt +32 -0
  30. kaia_foundation-3.7.3/kaia_foundation.egg-info/dependency_links.txt +1 -0
  31. kaia_foundation-3.7.3/kaia_foundation.egg-info/requires.txt +5 -0
  32. kaia_foundation-3.7.3/kaia_foundation.egg-info/top_level.txt +2 -0
  33. kaia_foundation-3.7.3/pyproject.toml +39 -0
  34. 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,6 @@
1
+ from .endpoint import endpoint
2
+ from .server import Server
3
+ from .api import Api, bind_to_api
4
+ from .test_api import TestApi
5
+ from .api_utils import ApiUtils
6
+ from .signature_processor import SignatureProcessor
@@ -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,9 @@
1
+ from typing import TypeVar, Generic
2
+ from abc import ABC, abstractmethod
3
+
4
+ T = TypeVar('T')
5
+
6
+ class IPrompter(ABC, Generic[T]):
7
+ @abstractmethod
8
+ def __call__(self, obj: T) -> str:
9
+ pass
@@ -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,5 @@
1
+ yo_fluq
2
+ requests
3
+ jsonpickle
4
+ Jinja2
5
+ flask
@@ -0,0 +1,2 @@
1
+ dist
2
+ foundation_kaia
@@ -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 = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+