lyrasense 0.0.1__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.
@@ -0,0 +1,164 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
163
+
164
+ lyrasense_cache.db
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024-present Dimitrios Kirtsios <dimkirts@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # Lyrasense Platform - Python SDK
2
+
3
+ This library is a Python SDK that can be used to interface with the Lyrasense Earth Observation(EO) platform.
4
+
5
+ ## Example Usage
6
+
7
+ First install the library in your environment:
8
+
9
+ ```bash
10
+ pip install lyrasense
11
+ ```
12
+
13
+ Then you need to login on the Lyrasense platform and create an API key.
14
+ To get access to the platform contact us at: <info@lyrasense.com>
15
+
16
+ <image>
17
+
18
+ Then you can the lyrasense decorators to wrap functions that you want to be deployed in the Lyrasense platform and execute them on demand.
19
+
20
+ ```python
21
+ from lyrasense import Lyrasense
22
+
23
+ # Initialize Lyrasense with your API base URL and key
24
+ Lyrasense.init(
25
+ "http://<base url>",
26
+ "<your api key>>",
27
+ )
28
+
29
+ # Use the environment configuration if needed
30
+ Lyrasense.env(
31
+ baseImage="python:3.9"
32
+ )
33
+
34
+ @Lyrasense.function
35
+ def addition(a, b):
36
+ return a + b
37
+
38
+
39
+ if __name__ == "__main__":
40
+ # If you call the wrapped function normally, it will be executed locally
41
+ result_local_def = addition(5, 3)
42
+ print(f"Local result: {result_local_def}")
43
+
44
+ # You can also explicitly call the wrapped function locally by using .local()
45
+ result_local = addition.local(5, 3)
46
+ print(f"Local result: {result_local}")
47
+
48
+ # You can execute the wrapped function remotely by using .remote()
49
+ # This will first deploy the function and then call it
50
+ result_remote = additionlowercase.remote(5, 3)
51
+ print(f"Remote result: {result_remote}")
52
+
53
+ ```
54
+
55
+ **Notes:**
56
+ - The initial function deployment needs some time to create the function, any further function calls should be fast.
57
+ - If the function code changes the updated function with the new code will be re-deployed.
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.3
2
+ Name: lyrasense
3
+ Version: 0.0.1
4
+ Summary: The Python SDK for the Lyrasense EO platform.
5
+ Project-URL: Homepage, https://www.lyrasense.com/
6
+ Author-email: Dimitrios Kirtsios <dimkirts@gmail.com>
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: requests>=2.25.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Lyrasense Platform - Python SDK
16
+
17
+ This library is a Python SDK that can be used to interface with the Lyrasense Earth Observation(EO) platform.
18
+
19
+ ## Example Usage
20
+
21
+ First install the library in your environment:
22
+
23
+ ```bash
24
+ pip install lyrasense
25
+ ```
26
+
27
+ Then you need to login on the Lyrasense platform and create an API key.
28
+ To get access to the platform contact us at: <info@lyrasense.com>
29
+
30
+ <image>
31
+
32
+ Then you can the lyrasense decorators to wrap functions that you want to be deployed in the Lyrasense platform and execute them on demand.
33
+
34
+ ```python
35
+ from lyrasense import Lyrasense
36
+
37
+ # Initialize Lyrasense with your API base URL and key
38
+ Lyrasense.init(
39
+ "http://<base url>",
40
+ "<your api key>>",
41
+ )
42
+
43
+ # Use the environment configuration if needed
44
+ Lyrasense.env(
45
+ baseImage="python:3.9"
46
+ )
47
+
48
+ @Lyrasense.function
49
+ def addition(a, b):
50
+ return a + b
51
+
52
+
53
+ if __name__ == "__main__":
54
+ # If you call the wrapped function normally, it will be executed locally
55
+ result_local_def = addition(5, 3)
56
+ print(f"Local result: {result_local_def}")
57
+
58
+ # You can also explicitly call the wrapped function locally by using .local()
59
+ result_local = addition.local(5, 3)
60
+ print(f"Local result: {result_local}")
61
+
62
+ # You can execute the wrapped function remotely by using .remote()
63
+ # This will first deploy the function and then call it
64
+ result_remote = additionlowercase.remote(5, 3)
65
+ print(f"Remote result: {result_remote}")
66
+
67
+ ```
68
+
69
+ **Notes:**
70
+ - The initial function deployment needs some time to create the function, any further function calls should be fast.
71
+ - If the function code changes the updated function with the new code will be re-deployed.
@@ -0,0 +1,83 @@
1
+ # Lyrasense Python SDK
2
+
3
+ This is the repository for the Python SDK that allows you to interface with the Lyrasense Earth Observation platform.
4
+
5
+ ## Packaging Instructions
6
+
7
+ Make sure you have access to an API key to push in PyPI, ask <dimkirts@gmail.com> for one.
8
+ Make sure you have installed PyPA's build, and Twine:
9
+
10
+ ```bash
11
+ python3 -m pip install --upgrade build
12
+ python3 -m pip install --upgrade twine
13
+ ```
14
+
15
+ Then build your library by running in the root of the project directory:
16
+
17
+ ```bash
18
+ python3 -m build
19
+ ```
20
+
21
+ This should create two files in a `dist` folder that look like:
22
+
23
+ ```txt
24
+ lyrasense-0.0.1.tar.gz
25
+ lyrasense-0.0.1-py3-none-any.whl
26
+ ```
27
+
28
+ Now to push these files to PyPI:
29
+
30
+ ```bash
31
+ python3 -m twine upload --repository pypi dist/*
32
+ ```
33
+
34
+ You are good to go!
35
+
36
+ Reference: <https://packaging.python.org/en/latest/tutorials/packaging-projects/>
37
+
38
+ ## Virtual Environment
39
+
40
+ We assume that you have Python installed on your system.
41
+
42
+ Create a fresh virtual environment:
43
+
44
+ ```bash
45
+ rm -rf .venv
46
+ python3 -m venv .venv
47
+ ```
48
+
49
+ Activate the virtual environment:
50
+
51
+ ```bash
52
+ source .venv/bin/activate
53
+ # confirm that you are in your venv
54
+ which python
55
+ ```
56
+
57
+ To exit your venv:
58
+
59
+ ```bash
60
+ deactivate
61
+ ```
62
+
63
+ ## Running locally on editable mode for testing
64
+
65
+ - Go to the root folder of the project
66
+ - Create a virtualenv and activate it
67
+ - Install the library in editable mode `pip install -e .`
68
+ - Now you can either execute the example, run or write unit tests
69
+
70
+ ## Installing the library and running the example
71
+
72
+ First create and activate a virtualenv as mentioned above.
73
+ Then install the library inside the activated virtualenv like this:
74
+
75
+ ```bash
76
+ pip install lyrasense
77
+ ```
78
+
79
+ Then run the example like this:
80
+
81
+ ```bash
82
+ python3 examples/example.py
83
+ ```
@@ -0,0 +1,26 @@
1
+ from lyrasense import Lyrasense
2
+
3
+ # Initialize Lyrasense with your API base URL and key
4
+ Lyrasense.init(
5
+ "http://35.214.166.85",
6
+ "w9eFPBG9D0K6mFLel_CgSTeYGDr8cyoSDwPhnJeblckb__jy1dD7LbHZCWnR",
7
+ debug=True,
8
+ )
9
+
10
+ # Use the environment configuration if needed
11
+ Lyrasense.env(baseImage="python:3.9")
12
+
13
+
14
+ @Lyrasense.function
15
+ def additionlowercase(a, b):
16
+ return a + b
17
+
18
+
19
+ if __name__ == "__main__":
20
+ # Use the function locally
21
+ result_local = additionlowercase.local(5, 3)
22
+ print(f"Local result: {result_local}")
23
+
24
+ # Use the function remotely
25
+ result_remote = additionlowercase.remote(5, 3)
26
+ print(f"Remote result: {result_remote}")
@@ -0,0 +1,235 @@
1
+ import sqlite3
2
+ import hashlib
3
+ import base64
4
+ import requests
5
+ import time
6
+ import random
7
+ import inspect
8
+ import re
9
+ from functools import wraps
10
+ ## TODO: Figure out if we can simply install the nuclio SDK without the jupyter baggage to avoid unused dependencies
11
+ import nuclio
12
+
13
+ class LyrasenseCache:
14
+ def __init__(self, db_path='lyrasense_cache.db'):
15
+ self.conn = sqlite3.connect(db_path)
16
+ self.create_table()
17
+
18
+ def create_table(self):
19
+ cursor = self.conn.cursor()
20
+ cursor.execute('''
21
+ CREATE TABLE IF NOT EXISTS function_cache (
22
+ function_name TEXT PRIMARY KEY,
23
+ function_hash TEXT,
24
+ deployed_url TEXT
25
+ )
26
+ ''')
27
+ self.conn.commit()
28
+
29
+ def get_function_hash(self, func):
30
+ func_code = inspect.getsource(func)
31
+ return base64.b64encode(hashlib.sha256(func_code.encode()).digest()).decode()
32
+
33
+ def get_deployed_url(self, func_name):
34
+ cursor = self.conn.cursor()
35
+ cursor.execute('SELECT deployed_url FROM function_cache WHERE function_name = ?', (func_name,))
36
+ result = cursor.fetchone()
37
+ return result[0] if result else None
38
+
39
+ def update_cache(self, func_name, func_hash, deployed_url):
40
+ cursor = self.conn.cursor()
41
+ cursor.execute('''
42
+ INSERT OR REPLACE INTO function_cache (function_name, function_hash, deployed_url)
43
+ VALUES (?, ?, ?)
44
+ ''', (func_name, func_hash, deployed_url))
45
+ self.conn.commit()
46
+
47
+ def is_deployment_needed(self, func):
48
+ func_name = func.__name__
49
+ new_hash = self.get_function_hash(func)
50
+ cursor = self.conn.cursor()
51
+ cursor.execute('SELECT function_hash FROM function_cache WHERE function_name = ?', (func_name,))
52
+ result = cursor.fetchone()
53
+ return result is None or result[0] != new_hash
54
+
55
+ def retry_on_exception(max_retries=10, delay=1, backoff=2, exceptions=(Exception,)):
56
+ def decorator(func):
57
+ @wraps(func)
58
+ def wrapper(*args, **kwargs):
59
+ retry_delay = delay
60
+ for attempt in range(max_retries):
61
+ try:
62
+ return func(*args, **kwargs)
63
+ except exceptions as e:
64
+ if attempt == max_retries - 1:
65
+ raise # Re-raise the last exception if all retries failed
66
+ print(f"Attempt {attempt + 1} failed: {str(e)}. Retrying in {retry_delay} seconds...")
67
+ time.sleep(retry_delay)
68
+ retry_delay *= backoff
69
+ # Add some jitter to avoid thundering herd problem
70
+ retry_delay += random.uniform(0, 1)
71
+ return wrapper
72
+ return decorator
73
+
74
+ @retry_on_exception(max_retries=3, delay=2, backoff=2, exceptions=(Exception,))
75
+ def deploy_function_with_retry(data):
76
+ print('[deploy_function] The config is: {}'.format(Lyrasense.config))
77
+ print('[deploy_function] The data is: {}'.format(data))
78
+ print("[deploy_function] This deploys the function!")
79
+
80
+ userfunction_def=Lyrasense.function_extraction(data['func_source'])
81
+ userfunction_name=data['func_name']
82
+
83
+ indented_function = '\n'.join(' ' + line for line in userfunction_def.split('\n'))
84
+
85
+
86
+ function_template = f'''
87
+ def handler(context, event):
88
+ body = event.body
89
+ context.logger.info('[lyrasense][{userfunction_name}] Executing function with args: ' + str(body['args']))
90
+
91
+ ## USER DEFINED CODE - START ##
92
+ {indented_function}
93
+
94
+ result = {userfunction_name}(*body['args'])
95
+ ## USER DEFINED CODE - END ##
96
+
97
+ return context.Response(body={{ "function": "{userfunction_name}", "result": result }},
98
+ headers={{}},
99
+ content_type='application/json',
100
+ status_code=200)
101
+ '''
102
+
103
+ print(function_template)
104
+
105
+ spec = nuclio.ConfigSpec(
106
+ config= {
107
+ 'spec.build.baseImage': 'python:3.9',
108
+ },
109
+ cmd=['pip install msgpack']
110
+ )
111
+
112
+ addr = nuclio.deploy_code(
113
+ function_template,
114
+ name='projectName-{}'.format(userfunction_name),
115
+ project='lyrasense',
116
+ verbose=True,
117
+ spec=spec
118
+ )
119
+
120
+ print(f"Deploying function: {data['func_name']}")
121
+ print(addr)
122
+
123
+ if Lyrasense.ENV == 'LOCAL':
124
+ pattern = r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)$'
125
+
126
+ # Replace IP with localhost, keeping the port
127
+ addr = re.sub(pattern, r'localhost:\2', addr[0])
128
+ return addr
129
+
130
+
131
+ """
132
+ TODO:
133
+ a) Security: Serializing and deserializing functions can be a security risk if not handled properly. Ensure that you have proper authentication and authorization mechanisms in place.
134
+ b) Error Handling: Add more robust error handling for network issues, API errors, etc.
135
+ c) Asynchronous Execution: Consider implementing an asynchronous approach for long-running tasks.
136
+ """
137
+ class LyrasenseFunction:
138
+ cache = LyrasenseCache()
139
+
140
+ def __init__(self, func):
141
+ self.func = func
142
+ self.deployed_url = None
143
+
144
+ def __call__(self, *args, **kwargs):
145
+ return self.func(*args, **kwargs)
146
+
147
+ def deploy(self):
148
+ if self.cache.is_deployment_needed(self.func):
149
+ print(f"Deploying function: {self.func.__name__}")
150
+
151
+ func_source = inspect.getsource(self.func)
152
+ data = {
153
+ "func_source": func_source,
154
+ "func_name": self.func.__name__,
155
+ }
156
+
157
+ self.deployed_url = 'http://' + str(Lyrasense.deploy_function(data))
158
+ func_hash = self.cache.get_function_hash(self.func)
159
+ self.cache.update_cache(self.func.__name__, func_hash, self.deployed_url)
160
+ else:
161
+ print(f"Function {self.func.__name__} is up to date, no deployment needed")
162
+ self.deployed_url = self.cache.get_deployed_url(self.func.__name__)
163
+ return self.deployed_url
164
+
165
+ def local(self, *args, **kwargs):
166
+ return self.func(*args, **kwargs)
167
+
168
+ def remote(self, *args, **kwargs):
169
+ if not self.deployed_url:
170
+ #raise ValueError("Function hasn't been deployed yet. Call .deploy() first.")
171
+ self.deploy()
172
+
173
+ data = {
174
+ "args": args,
175
+ "kwargs": kwargs
176
+ }
177
+
178
+ response = requests.post(self.deployed_url, json=data)
179
+
180
+ if response.status_code == 200:
181
+ return response.json()
182
+ else:
183
+ raise Exception(f"Remote execution failed: {response.text}")
184
+
185
+ class Lyrasense:
186
+ # Config
187
+ ## TODO: Add logic to switch environments
188
+ ENV = 'LOCAL'
189
+ config = { 'env': 'Python', 'baseImage': 'None' }
190
+
191
+ def env(baseImage='python'):
192
+ print('[env] Defines an environment, that you can use to attach functions on it')
193
+ print('[env] The config is: {}'.format(Lyrasense.config))
194
+ Lyrasense.config['baseImage'] = baseImage
195
+
196
+ @staticmethod
197
+ def function_extraction(function_code):
198
+ # The updated regex pattern
199
+ pattern = r'(def\s+.*?$)(?:\n(\s+).*?)*$'
200
+
201
+ # Find the match
202
+ match = re.search(pattern, function_code, re.MULTILINE | re.DOTALL)
203
+
204
+ if match:
205
+ # Extract the function definition
206
+ function_lines = match.group(0).split('\n')
207
+
208
+ # Get the indentation of the first line after the function definition
209
+ if len(function_lines) > 1:
210
+ indentation = re.match(r'\s*', function_lines[1]).group(0)
211
+ else:
212
+ indentation = ''
213
+
214
+ # Join the lines, preserving indentation
215
+ extracted_function = '\n'.join([function_lines[0]] + [line if line.startswith(indentation) else indentation + line for line in function_lines[1:]])
216
+
217
+ print("Extracted function:")
218
+ print(extracted_function)
219
+ return extracted_function
220
+ else:
221
+ raise Exception('Something went wrong with the function extraction!')
222
+
223
+
224
+ @staticmethod
225
+ def function_code(func):
226
+ return LyrasenseFunction(func)
227
+
228
+ @staticmethod
229
+ def deploy_function(data):
230
+ try:
231
+ return deploy_function_with_retry(data)
232
+ except Exception as e:
233
+ print(f"Deployment failed after multiple attempts: {str(e)}")
234
+ # Here you can add any additional error handling or user notification
235
+ raise
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ build-backend = "hatchling.build"
3
+ requires = ["hatchling"]
4
+
5
+ [project]
6
+ authors = [
7
+ {name = "Dimitrios Kirtsios", email = "dimkirts@gmail.com"},
8
+ ]
9
+ classifiers = [
10
+ "Programming Language :: Python :: 3",
11
+ "License :: OSI Approved :: MIT License",
12
+ "Operating System :: OS Independent",
13
+ ]
14
+ dependencies = [
15
+ "requests>=2.25.0",
16
+ ]
17
+ description = "The Python SDK for the Lyrasense EO platform."
18
+ name = "lyrasense"
19
+ readme = "PACKAGE_README.md"
20
+ requires-python = ">=3.11"
21
+ version = "0.0.1"
22
+
23
+ [project.urls]
24
+ "Homepage" = "https://www.lyrasense.com/"
@@ -0,0 +1,3 @@
1
+ from .lyrasense import Lyrasense
2
+
3
+ __all__ = ["Lyrasense"]
@@ -0,0 +1,155 @@
1
+ import requests
2
+ import time
3
+ import inspect
4
+ import re
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class LyrasenseFunction:
11
+ def __init__(self, func):
12
+ self.func = func
13
+ self.function_id = None
14
+ self.deployed_url = None
15
+
16
+ def __call__(self, *args, **kwargs):
17
+ return self.func(*args, **kwargs)
18
+
19
+ def _function_extraction(self, function_code):
20
+ # The updated regex pattern
21
+ pattern = r"(def\s+.*?$)(?:\n(\s+).*?)*$"
22
+
23
+ # Find the match
24
+ match = re.search(pattern, function_code, re.MULTILINE | re.DOTALL)
25
+
26
+ if match:
27
+ # Extract the function definition
28
+ function_lines = match.group(0).split("\n")
29
+
30
+ # Get the indentation of the first line after the function definition
31
+ if len(function_lines) > 1:
32
+ indentation = re.match(r"\s*", function_lines[1]).group(0)
33
+ else:
34
+ indentation = ""
35
+
36
+ # Join the lines, preserving indentation
37
+ extracted_function = "\n".join(
38
+ [function_lines[0]]
39
+ + [
40
+ line if line.startswith(indentation) else indentation + line
41
+ for line in function_lines[1:]
42
+ ]
43
+ )
44
+
45
+ logger.debug("Extracted function:\n%s", extracted_function)
46
+ return extracted_function
47
+ else:
48
+ raise Exception("Something went wrong with the function extraction!")
49
+
50
+ def deploy(self):
51
+ func_source = self._function_extraction(inspect.getsource(self.func))
52
+
53
+ logger.debug("In deploy, func source:\n%s", func_source)
54
+ data = {
55
+ "name": self.func.__name__,
56
+ "code": func_source,
57
+ "runtime_config": {"PYTHON_VERSION": "3.11"},
58
+ }
59
+
60
+ logger.debug("In deploy calling PUT /api/v1/functions/")
61
+ response = requests.put(
62
+ f"{Lyrasense.api_base_url}/api/v1/functions/",
63
+ json=data,
64
+ headers={"api-key": Lyrasense.api_key},
65
+ )
66
+ response.raise_for_status()
67
+
68
+ function_data = response.json()
69
+
70
+ logger.debug("In deploy PUT response: %s", function_data)
71
+ self.function_id = function_data["id"]
72
+
73
+ # Poll until the function is deployed or deployment fails
74
+ while True:
75
+ # TODO: FIX: LRS-84
76
+ time.sleep(5)
77
+
78
+ logger.debug("In deploy, started polling, calling GET")
79
+ status = self.get_function_status()
80
+
81
+ logger.debug("Poll response: %s", status)
82
+ if status == "deployed":
83
+ self.deployed_url = function_data["deployed_url"]
84
+ logger.info("Function %s deployed successfully.", self.func.__name__)
85
+ break
86
+ elif status == "deployment_failed":
87
+ raise DeploymentFailedException(
88
+ f"Deployment failed for function {self.func.__name__}"
89
+ )
90
+
91
+ return self.deployed_url
92
+
93
+ def get_function_status(self):
94
+ response = requests.get(
95
+ f"{Lyrasense.api_base_url}/api/v1/functions/{self.function_id}",
96
+ headers={"api-key": Lyrasense.api_key},
97
+ )
98
+ response.raise_for_status()
99
+
100
+ return response.json()["status"]
101
+
102
+ def local(self, *args, **kwargs):
103
+ return self.func(*args, **kwargs)
104
+
105
+ def remote(self, *args, **kwargs):
106
+ if not self.function_id:
107
+ logger.debug("In remote method - no function id, calling deploy")
108
+ self.deploy()
109
+
110
+ logger.debug("In remote method - function id present, calling execute")
111
+ data = {"args": args or kwargs}
112
+
113
+ response = requests.post(
114
+ f"{Lyrasense.api_base_url}/api/v1/functions/{self.function_id}/execute",
115
+ json=data,
116
+ headers={"api-key": Lyrasense.api_key},
117
+ )
118
+ response.raise_for_status()
119
+ return response.json()["result"]
120
+
121
+
122
+ class Lyrasense:
123
+ api_base_url = None
124
+ api_key = None
125
+ debug = False
126
+
127
+ @staticmethod
128
+ def init(api_base_url, api_key, debug=False):
129
+ Lyrasense.api_base_url = api_base_url
130
+ Lyrasense.api_key = api_key
131
+ Lyrasense.debug = debug
132
+
133
+ logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
134
+ logger.setLevel(logging.DEBUG if debug else logging.INFO)
135
+
136
+ logger.info("Lyrasense initialized with API base URL: %s", api_base_url)
137
+ if debug:
138
+ logger.debug("Debug mode is ON")
139
+
140
+ @staticmethod
141
+ def function(func):
142
+ if not Lyrasense.api_base_url or not Lyrasense.api_key:
143
+ raise ValueError(
144
+ "Lyrasense has not been initialized. Call Lyrasense.init() first."
145
+ )
146
+ return LyrasenseFunction(func)
147
+
148
+ @staticmethod
149
+ def env(baseImage="python"):
150
+ # We can add env specific params here in the future.
151
+ pass
152
+
153
+
154
+ class DeploymentFailedException(Exception):
155
+ pass
@@ -0,0 +1,22 @@
1
+ import unittest
2
+ from unittest.mock import patch
3
+ from src.lib import Lyrasense
4
+
5
+ class TestLyrasenseLib(unittest.TestCase):
6
+
7
+ @patch('requests.post')
8
+ def test_simple_function(self, mock_post):
9
+ mock_post.return_value.status_code = 200
10
+ mock_post.return_value.json.return_value = {"job_id": "1234"}
11
+
12
+ @Lyrasense.function
13
+ def add(a, b):
14
+ return a + b
15
+
16
+ job_id = add(3, 5)
17
+ self.assertEqual(job_id, "1234")
18
+ mock_post.assert_called_once()
19
+
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()