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.
- lyrasense-0.0.1/.gitignore +164 -0
- lyrasense-0.0.1/LICENSE +20 -0
- lyrasense-0.0.1/PACKAGE_README.md +57 -0
- lyrasense-0.0.1/PKG-INFO +71 -0
- lyrasense-0.0.1/README.md +83 -0
- lyrasense-0.0.1/examples/example.py +26 -0
- lyrasense-0.0.1/old/lib.py +235 -0
- lyrasense-0.0.1/pyproject.toml +24 -0
- lyrasense-0.0.1/src/lyrasense/__init__.py +3 -0
- lyrasense-0.0.1/src/lyrasense/lyrasense.py +155 -0
- lyrasense-0.0.1/tests/test__lib.py +22 -0
|
@@ -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
|
lyrasense-0.0.1/LICENSE
ADDED
|
@@ -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.
|
lyrasense-0.0.1/PKG-INFO
ADDED
|
@@ -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,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()
|