pam-python 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pam_python-0.1.0/LICENSE.txt +17 -0
- pam_python-0.1.0/PKG-INFO +29 -0
- pam_python-0.1.0/README.md +3 -0
- pam_python-0.1.0/pam/__init__.py +0 -0
- pam_python-0.1.0/pam/api.py +42 -0
- pam_python-0.1.0/pam/cli.py +107 -0
- pam_python-0.1.0/pam/interface_task_manager.py +30 -0
- pam_python-0.1.0/pam/models/__init__.py +0 -0
- pam_python-0.1.0/pam/models/request_command.py +138 -0
- pam_python-0.1.0/pam/server.py +170 -0
- pam_python-0.1.0/pam/service.py +101 -0
- pam_python-0.1.0/pam/task_manager.py +141 -0
- pam_python-0.1.0/pam/temp_file_utils.py +75 -0
- pam_python-0.1.0/pam/templates/buildcmd/pamb +31 -0
- pam_python-0.1.0/pam/templates/buildcmd/pamb-base.sh +20 -0
- pam_python-0.1.0/pam/templates/docker/Dockerfile +17 -0
- pam_python-0.1.0/pam/templates/init/main.tmpl +11 -0
- pam_python-0.1.0/pam/templates/service/ServiceClass.tmpl +48 -0
- pam_python-0.1.0/pam/templates/service/functions.tmpl +22 -0
- pam_python-0.1.0/pam/templates/service/service.test.tmpl +83 -0
- pam_python-0.1.0/pam/templates/service/service.yaml +2 -0
- pam_python-0.1.0/pam/tester_task.py +60 -0
- pam_python-0.1.0/pam/utils.py +38 -0
- pam_python-0.1.0/pam_python.egg-info/PKG-INFO +29 -0
- pam_python-0.1.0/pam_python.egg-info/SOURCES.txt +29 -0
- pam_python-0.1.0/pam_python.egg-info/dependency_links.txt +1 -0
- pam_python-0.1.0/pam_python.egg-info/entry_points.txt +2 -0
- pam_python-0.1.0/pam_python.egg-info/requires.txt +7 -0
- pam_python-0.1.0/pam_python.egg-info/top_level.txt +1 -0
- pam_python-0.1.0/setup.cfg +4 -0
- pam_python-0.1.0/setup.py +41 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
This software is provided as part of the agreement between 3dsinteractive,co.ltd ("the Provider") and its customers ("the Licensee") under the following conditions:
|
|
2
|
+
|
|
3
|
+
1. **Usage Rights for Active Customers**:
|
|
4
|
+
- The Licensee may use, modify, and distribute this software exclusively for their internal operations in connection with the services provided by the Provider.
|
|
5
|
+
- No disclosure of source code is required while the Licensee maintains active status as a customer.
|
|
6
|
+
|
|
7
|
+
2. **Termination of Customer Status**:
|
|
8
|
+
- Upon termination of the agreement between the Provider and the Licensee, the Licensee must either:
|
|
9
|
+
a. Cease using and delete all copies of this software, or
|
|
10
|
+
b. Open-source any derived works based on this software under an OSI-approved license.
|
|
11
|
+
|
|
12
|
+
3. **Restrictions**:
|
|
13
|
+
- Redistribution of this software to third parties not under the Licensee's operational control is strictly prohibited.
|
|
14
|
+
- The Licensee may not use this software for competitive purposes against the Provider.
|
|
15
|
+
|
|
16
|
+
4. **Liability**:
|
|
17
|
+
- The Provider provides this software "as is" without warranty of any kind, either expressed or implied.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pam-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pam Python Library
|
|
5
|
+
Home-page: https://github.com/heart/pam-python
|
|
6
|
+
Author: Narongrit Kanhanoi
|
|
7
|
+
Author-email: narongrit@pams.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: License :: Other/Proprietary License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Customer Service
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
Requires-Dist: setuptools>=70.0.0
|
|
20
|
+
Requires-Dist: Flask>=3.0.2
|
|
21
|
+
Requires-Dist: aiohttp>=3.11.11
|
|
22
|
+
Requires-Dist: pandas>=2.2.3
|
|
23
|
+
Requires-Dist: Faker>=33.1.0
|
|
24
|
+
Requires-Dist: dask>=2024.12.1
|
|
25
|
+
Requires-Dist: dask-expr>=1.1.21
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
This library is distributed under a custom license. Active customers of [Your Company Name] may use it as per the terms stated in the `LICENSE.txt` file. For details, contact contact@3dsinteractive.com.
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from pam.utils import log
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class API:
|
|
6
|
+
|
|
7
|
+
def http_post(self, url, data):
|
|
8
|
+
"""
|
|
9
|
+
Sends an HTTP POST request to the specified URL with the given data as JSON.
|
|
10
|
+
|
|
11
|
+
:param url: The URL to send the POST request to.
|
|
12
|
+
:param data: A dictionary to be used as the JSON body of the POST request.
|
|
13
|
+
:return: The response from the server.
|
|
14
|
+
"""
|
|
15
|
+
headers = {
|
|
16
|
+
'Content-Type': 'application/json'}
|
|
17
|
+
try:
|
|
18
|
+
response = requests.post(
|
|
19
|
+
url, json=data, timeout=30, headers=headers)
|
|
20
|
+
response.raise_for_status() # Raise an exception for HTTP errors
|
|
21
|
+
return response
|
|
22
|
+
except requests.RequestException as e:
|
|
23
|
+
log(f"HTTP POST request failed: {e}")
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def http_upload(self, url, file_path):
|
|
27
|
+
"""
|
|
28
|
+
Uploads a file to the specified URL.
|
|
29
|
+
|
|
30
|
+
:param url: The URL to upload the file to.
|
|
31
|
+
:param file_path: The path to the file to be uploaded.
|
|
32
|
+
:return: The response from the server.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
with open(file_path, 'rb') as file:
|
|
36
|
+
files = {'file': file}
|
|
37
|
+
response = requests.post(url, files=files, timeout=300)
|
|
38
|
+
response.raise_for_status() # Raise an exception for HTTP errors
|
|
39
|
+
return response
|
|
40
|
+
except requests.RequestException as e:
|
|
41
|
+
log(f"File upload failed: {e}")
|
|
42
|
+
return None
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import pkg_resources
|
|
4
|
+
import shutil
|
|
5
|
+
import glob
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
args = sys.argv[1:]
|
|
10
|
+
if len(args) == 0:
|
|
11
|
+
print("Usage: pam <command> [args]")
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
cmd = args[0]
|
|
15
|
+
|
|
16
|
+
if cmd == "init":
|
|
17
|
+
init_project()
|
|
18
|
+
elif cmd == "new":
|
|
19
|
+
type = args[1]
|
|
20
|
+
if type == "service":
|
|
21
|
+
name = args[2]
|
|
22
|
+
create_service(name)
|
|
23
|
+
elif cmd == "test":
|
|
24
|
+
if len(args) < 2:
|
|
25
|
+
print("Usage: pam test <modulename>")
|
|
26
|
+
return
|
|
27
|
+
module_name = args[1]
|
|
28
|
+
test_module(module_name)
|
|
29
|
+
else:
|
|
30
|
+
print(f"Unknown command: {args.command}")
|
|
31
|
+
|
|
32
|
+
def test_module(module_name):
|
|
33
|
+
"""Run all test files matching '*.test.py' in the specified module directory."""
|
|
34
|
+
# Locate all test files in the module directory
|
|
35
|
+
test_files = glob.glob(f"{module_name}/*.test.py")
|
|
36
|
+
|
|
37
|
+
if not test_files:
|
|
38
|
+
print(f"Error: No test files found in module '{module_name}' (expected '*.test.py').")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
print(f"Found {len(test_files)} test file(s) in '{module_name}':")
|
|
42
|
+
for test_file in test_files:
|
|
43
|
+
print(f" - {test_file}")
|
|
44
|
+
|
|
45
|
+
# Run each test file
|
|
46
|
+
for test_file in test_files:
|
|
47
|
+
print(f"\nRunning tests in {test_file}...")
|
|
48
|
+
result = subprocess.run([sys.executable, test_file], capture_output=True, text=True)
|
|
49
|
+
|
|
50
|
+
print("\nTest Output:")
|
|
51
|
+
print(result.stdout)
|
|
52
|
+
if result.stderr:
|
|
53
|
+
print("\nErrors:")
|
|
54
|
+
print(result.stderr)
|
|
55
|
+
print("-" * 40) # Separator for clarity
|
|
56
|
+
|
|
57
|
+
def cpy(src, dest):
|
|
58
|
+
template_dir = pkg_resources.resource_filename("pam", "templates")
|
|
59
|
+
src_file = os.path.join(template_dir, src)
|
|
60
|
+
shutil.copy(src_file, dest)
|
|
61
|
+
|
|
62
|
+
def replace_template_content(service_name, file_name):
|
|
63
|
+
file_path = os.path.join(service_name, file_name)
|
|
64
|
+
with open(file_path, 'r+') as file:
|
|
65
|
+
filedata = file.read()
|
|
66
|
+
updated_data = filedata.replace('#CLASS_NAME#', service_name)
|
|
67
|
+
file.seek(0) # Move the file pointer to the beginning of the file
|
|
68
|
+
file.write(updated_data)
|
|
69
|
+
file.truncate() # Remove any leftover content after the replacement
|
|
70
|
+
|
|
71
|
+
def create_service(name):
|
|
72
|
+
if os.path.exists(name):
|
|
73
|
+
response = input(f"Service {name} already exists. Do you want to overwrite it? (y/N): ").strip().lower()
|
|
74
|
+
if response == 'y':
|
|
75
|
+
shutil.rmtree(name)
|
|
76
|
+
else:
|
|
77
|
+
print("Cancelled.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
os.mkdir(name)
|
|
81
|
+
open(os.path.join(name, "__init__.py"), 'a', encoding='utf-8').close()
|
|
82
|
+
cpy("service/ServiceClass.tmpl", os.path.join(name, name+"_service.py") )
|
|
83
|
+
cpy("service/service.yaml", os.path.join(name, "service.yaml") )
|
|
84
|
+
cpy("service/functions.tmpl", os.path.join(name, "functions.py") )
|
|
85
|
+
cpy("service/service.test.tmpl", os.path.join(name, name+".test.py") )
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
replace_template_content(name, name+"_service.py")
|
|
89
|
+
replace_template_content(name, "service.yaml")
|
|
90
|
+
replace_template_content(name, name+".test.py")
|
|
91
|
+
|
|
92
|
+
print(f"Service {name} created.")
|
|
93
|
+
print(f"Run `pam test {name}` to run tests for the service.")
|
|
94
|
+
|
|
95
|
+
def init_project():
|
|
96
|
+
cpy("init/main.tmpl", "main.py")
|
|
97
|
+
cpy("docker/Dockerfile", "Dockerfile")
|
|
98
|
+
cpy("buildcmd/pamb", "pamb")
|
|
99
|
+
cpy("buildcmd/pamb-base.sh", "pamb-base.sh")
|
|
100
|
+
if not os.path.exists("requirements.txt"):
|
|
101
|
+
open("requirements.txt", 'a', encoding='utf-8').close()
|
|
102
|
+
|
|
103
|
+
with open("requirements.txt", "w", encoding='utf-8') as f:
|
|
104
|
+
subprocess.run(["pip", "freeze"], stdout=f, check=True)
|
|
105
|
+
|
|
106
|
+
print("--- Welcome to PAM ---\n")
|
|
107
|
+
print("To create a new servive run\n`pam new service <service_name>`\n\n")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pam.models.request_command import RequestCommand
|
|
3
|
+
from pam.service import Service
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ITaskManager(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def on_dataset_input(self, req: RequestCommand):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def start_service(self, service_class, req: RequestCommand, service_name):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def terminate_service(self, token):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def service_request_data(self, service: Service, page):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def service_upload_result(self, service: Service, file_path):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def service_upload_report(self, service: Service, file_path):
|
|
30
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
RequestCommand is object that Wrap the http request to object that simple to access the parameters
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from pam.utils import log
|
|
11
|
+
from pam.temp_file_utils import TempfileUtils
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class RequestCommand:
|
|
16
|
+
"""
|
|
17
|
+
RequestCommand is object that Wrap the http request
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
token: str
|
|
21
|
+
cmd: str
|
|
22
|
+
data_request_api: str
|
|
23
|
+
response_api: str
|
|
24
|
+
is_end: bool
|
|
25
|
+
next: str
|
|
26
|
+
input_files: list[str]
|
|
27
|
+
service_name: str
|
|
28
|
+
|
|
29
|
+
def is_start_command(self):
|
|
30
|
+
"""Determine is this command is start command"""
|
|
31
|
+
return self.cmd == "start"
|
|
32
|
+
|
|
33
|
+
def is_dataset_command(self):
|
|
34
|
+
"""Determine is this command is dataset command"""
|
|
35
|
+
return self.cmd == "dataset"
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def __safe_str_to_bool(value):
|
|
39
|
+
if isinstance(value, bool):
|
|
40
|
+
return value
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
if value.lower() in ['true', '1', 'yes']:
|
|
43
|
+
return True
|
|
44
|
+
if value.lower() in ['false', '0', 'no']:
|
|
45
|
+
return False
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def __extract_data_from_request(request, keys):
|
|
50
|
+
result = {}
|
|
51
|
+
|
|
52
|
+
if request.content_type == 'application/json':
|
|
53
|
+
data = request.get_json()
|
|
54
|
+
for key in keys:
|
|
55
|
+
if key in data:
|
|
56
|
+
result[key] = data[key]
|
|
57
|
+
else:
|
|
58
|
+
result[key] = ""
|
|
59
|
+
elif request.content_type.startswith('multipart/form-data'):
|
|
60
|
+
for key in keys:
|
|
61
|
+
data_str = request.form.get(key, '')
|
|
62
|
+
if data_str:
|
|
63
|
+
result[key] = data_str
|
|
64
|
+
else:
|
|
65
|
+
result[key] = ""
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def __allowed_file(filename):
|
|
71
|
+
return '.' in filename and filename.rsplit(
|
|
72
|
+
'.', 1)[1].lower() in {'zip'}
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def __extract_input_file(zip_file, service_name, token):
|
|
76
|
+
|
|
77
|
+
if zip_file.filename == '':
|
|
78
|
+
log("handle_file_upload 'No selected file'")
|
|
79
|
+
return ([], "No selected file")
|
|
80
|
+
|
|
81
|
+
if not RequestCommand.__allowed_file(zip_file.filename):
|
|
82
|
+
log("handle_file_upload 'Invalid file type'")
|
|
83
|
+
return ([], "Invalid file type")
|
|
84
|
+
|
|
85
|
+
zip_file_dir = TempfileUtils.get_temp_file_name(
|
|
86
|
+
service_name, token, "dataset_")
|
|
87
|
+
|
|
88
|
+
zip_file_name = f"{zip_file_dir}.zip"
|
|
89
|
+
zip_file.save(zip_file_name)
|
|
90
|
+
|
|
91
|
+
extract_dir = Path(zip_file_dir)
|
|
92
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
with zipfile.ZipFile(zip_file_name, 'r') as zip_ref:
|
|
95
|
+
|
|
96
|
+
zip_ref.extractall(zip_file_dir)
|
|
97
|
+
|
|
98
|
+
if os.path.exists(f"{extract_dir}/__MACOSX"):
|
|
99
|
+
os.rmdir(f"{extract_dir}/__MACOSX")
|
|
100
|
+
if os.path.exists(f"{extract_dir}/.DS_Store"):
|
|
101
|
+
os.rmdir(f"{extract_dir}/.DS_Store")
|
|
102
|
+
|
|
103
|
+
input_files = sorted([os.path.join(extract_dir, name)
|
|
104
|
+
for name in os.listdir(extract_dir)])
|
|
105
|
+
|
|
106
|
+
log(f"Extract dir: {extract_dir}")
|
|
107
|
+
log(f"File save to: {input_files}")
|
|
108
|
+
|
|
109
|
+
return (input_files, "")
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def parse(request, service_name):
|
|
113
|
+
"""Create RequestCommand object from http request"""
|
|
114
|
+
params = RequestCommand.__extract_data_from_request(
|
|
115
|
+
request, ["cmd", "token", "data", "response", "is_end", "next"])
|
|
116
|
+
|
|
117
|
+
token = params["token"]
|
|
118
|
+
cmd = params["cmd"]
|
|
119
|
+
data = params["data"]
|
|
120
|
+
response = params["response"]
|
|
121
|
+
is_end = RequestCommand.__safe_str_to_bool(params["is_end"])
|
|
122
|
+
next_page = params["next"]
|
|
123
|
+
input_files = []
|
|
124
|
+
error_message = ""
|
|
125
|
+
|
|
126
|
+
if not token:
|
|
127
|
+
error_message = 'The `token` parameter is required.'
|
|
128
|
+
else:
|
|
129
|
+
if cmd == "dataset":
|
|
130
|
+
zip_file = request.files['file']
|
|
131
|
+
(files, error) = RequestCommand.__extract_input_file(
|
|
132
|
+
zip_file, service_name, token)
|
|
133
|
+
error_message = error
|
|
134
|
+
input_files = files
|
|
135
|
+
|
|
136
|
+
return RequestCommand(
|
|
137
|
+
token, cmd, data, response, is_end, next_page, input_files, service_name
|
|
138
|
+
), error_message
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Plugin Server
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import importlib
|
|
7
|
+
import yaml
|
|
8
|
+
from setuptools import find_packages
|
|
9
|
+
from flask import request, make_response, Flask, jsonify
|
|
10
|
+
from pam.utils import log
|
|
11
|
+
from pam.models.request_command import RequestCommand
|
|
12
|
+
from pam.task_manager import TaskManager
|
|
13
|
+
from pam.temp_file_utils import TempfileUtils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Server:
|
|
17
|
+
"""
|
|
18
|
+
Data Plugin Server
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
app: Flask
|
|
22
|
+
servicePool = {}
|
|
23
|
+
|
|
24
|
+
def __init__(self, app):
|
|
25
|
+
self.task_manager = TaskManager(self)
|
|
26
|
+
self.app = app
|
|
27
|
+
self.register_service()
|
|
28
|
+
self.task_manager.start_service_monitoring_schedul()
|
|
29
|
+
|
|
30
|
+
@app.route('/', methods=['GET'])
|
|
31
|
+
def home():
|
|
32
|
+
return self.response_ok({'message': "Hello, Data plugin v2."})
|
|
33
|
+
|
|
34
|
+
@app.route('/service/<service_name>', methods=['POST'])
|
|
35
|
+
def run(service_name):
|
|
36
|
+
TempfileUtils.clean_temp()
|
|
37
|
+
|
|
38
|
+
req, error_request = RequestCommand.parse(request, service_name)
|
|
39
|
+
|
|
40
|
+
if error_request != "":
|
|
41
|
+
response = {
|
|
42
|
+
'message': error_request
|
|
43
|
+
}
|
|
44
|
+
return self.response_error(response, 400)
|
|
45
|
+
|
|
46
|
+
response, code = self.handle_service_cmd(service_name, req)
|
|
47
|
+
return self.response_error(response, code)
|
|
48
|
+
|
|
49
|
+
@app.route('/tasks', methods=['GET'])
|
|
50
|
+
def tasks():
|
|
51
|
+
return self.response_ok({'message': "OK"})
|
|
52
|
+
|
|
53
|
+
@app.route('/status/<service_name>', methods=['GET'])
|
|
54
|
+
def ping(service_name):
|
|
55
|
+
# TODO: check the service status
|
|
56
|
+
return self.response_ok({'message': f"OK {service_name}"})
|
|
57
|
+
|
|
58
|
+
def get_service_class(self, service_name):
|
|
59
|
+
"""
|
|
60
|
+
Get service class from service name
|
|
61
|
+
"""
|
|
62
|
+
if service_name in self.servicePool:
|
|
63
|
+
return self.servicePool[service_name]
|
|
64
|
+
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def handle_service_cmd(self, service_name, req: RequestCommand):
|
|
68
|
+
"""
|
|
69
|
+
Handle all cmd type
|
|
70
|
+
"""
|
|
71
|
+
service_class = self.get_service_class(service_name)
|
|
72
|
+
|
|
73
|
+
if service_class is not None:
|
|
74
|
+
if req.is_start_command():
|
|
75
|
+
log(f"Request on start {service_name} token={req.token}")
|
|
76
|
+
return self.on_start(service_class, req, service_name)
|
|
77
|
+
|
|
78
|
+
if req.is_dataset_command():
|
|
79
|
+
|
|
80
|
+
log(f"""Request on dataset {service_name} token={
|
|
81
|
+
req.token} files={req.input_files}""")
|
|
82
|
+
|
|
83
|
+
return self.on_dataset(req)
|
|
84
|
+
|
|
85
|
+
response = {'message': f'Service `{service_name}` Notfound. '}
|
|
86
|
+
return response, 404
|
|
87
|
+
|
|
88
|
+
def on_start(self, service_class, req, service_name):
|
|
89
|
+
"""
|
|
90
|
+
start a service
|
|
91
|
+
"""
|
|
92
|
+
log(f"{service_name} -> on_start")
|
|
93
|
+
self.task_manager.start_service(service_class, req, service_name)
|
|
94
|
+
return {'acknowledge': True}, 200
|
|
95
|
+
|
|
96
|
+
def on_dataset(self, req):
|
|
97
|
+
"""
|
|
98
|
+
task_manager handle cmd=dataset
|
|
99
|
+
"""
|
|
100
|
+
log(f"{req.service_name} -> on_dataset")
|
|
101
|
+
self.task_manager.on_dataset_input(req)
|
|
102
|
+
return {'acknowledge': True}, 200
|
|
103
|
+
|
|
104
|
+
def on_register_service(self, service, end_point):
|
|
105
|
+
"""
|
|
106
|
+
register a service
|
|
107
|
+
"""
|
|
108
|
+
log(f"register service \"{service}\", endpoint: http://localhost/service/{end_point}")
|
|
109
|
+
self.servicePool[end_point] = service
|
|
110
|
+
|
|
111
|
+
def run(self, host='0.0.0.0', port=8000):
|
|
112
|
+
"""
|
|
113
|
+
run flask app
|
|
114
|
+
"""
|
|
115
|
+
self.app.run(host=host, port=port)
|
|
116
|
+
|
|
117
|
+
def response_error(self, json, code):
|
|
118
|
+
"""
|
|
119
|
+
create error response with custom http status code
|
|
120
|
+
"""
|
|
121
|
+
data = jsonify(json)
|
|
122
|
+
return make_response(data, code)
|
|
123
|
+
|
|
124
|
+
def response_ok(self, json, code=200):
|
|
125
|
+
"""
|
|
126
|
+
create response object with 200OK
|
|
127
|
+
"""
|
|
128
|
+
data = jsonify(json)
|
|
129
|
+
return make_response(data, code)
|
|
130
|
+
|
|
131
|
+
def get_service_config(self, package):
|
|
132
|
+
"""
|
|
133
|
+
load the service config from service.yml
|
|
134
|
+
"""
|
|
135
|
+
config_path_1 = f'{package}/service.yaml'
|
|
136
|
+
config_path_2 = f'{package}/service.yml'
|
|
137
|
+
if os.path.exists(config_path_1) and os.path.isfile(config_path_1):
|
|
138
|
+
with open(config_path_1, 'r', encoding='utf-8') as file:
|
|
139
|
+
config = yaml.safe_load(file)
|
|
140
|
+
return config
|
|
141
|
+
elif os.path.exists(config_path_2) and os.path.isfile(config_path_2):
|
|
142
|
+
with open(config_path_2, 'r', encoding='utf-8') as file:
|
|
143
|
+
config = yaml.safe_load(file)
|
|
144
|
+
return config
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def register_service(self):
|
|
148
|
+
"""
|
|
149
|
+
scan project to register all plugin services
|
|
150
|
+
"""
|
|
151
|
+
packages = find_packages()
|
|
152
|
+
for package in packages:
|
|
153
|
+
config = self.get_service_config(package)
|
|
154
|
+
if config is not None:
|
|
155
|
+
endpoint = config['endpoint']
|
|
156
|
+
class_name = config['class']
|
|
157
|
+
module = importlib.import_module(
|
|
158
|
+
f'{package}.{class_name}')
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
class_ = getattr(module, class_name)
|
|
162
|
+
except AttributeError:
|
|
163
|
+
log(
|
|
164
|
+
f"Error: Class '{class_name}' not found in module '{module}'.")
|
|
165
|
+
class_ = None
|
|
166
|
+
|
|
167
|
+
if endpoint.startswith("/"):
|
|
168
|
+
endpoint = endpoint[1:]
|
|
169
|
+
|
|
170
|
+
self.on_register_service(class_, endpoint)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service base class
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import uuid
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
import json
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import dask.dataframe as dd
|
|
13
|
+
from pam.utils import log, deep_convert_numbers_to_strings, get_adapter_id
|
|
14
|
+
from pam.models.request_command import RequestCommand
|
|
15
|
+
from pam.temp_file_utils import TempfileUtils
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pam.task_manager import TaskManager
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Service:
|
|
22
|
+
"""
|
|
23
|
+
Service base class
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# pylint: disable=too-many-instance-attributes
|
|
27
|
+
def __init__(self, task_manager: 'TaskManager', req: RequestCommand):
|
|
28
|
+
self.request = req
|
|
29
|
+
self.task_manager = task_manager
|
|
30
|
+
|
|
31
|
+
# == REQUEST DATA ===
|
|
32
|
+
def _request_data(self, page=""):
|
|
33
|
+
log(f"{self.request.service_name}: Request data page={page}")
|
|
34
|
+
self.task_manager.service_request_data(self, page)
|
|
35
|
+
|
|
36
|
+
# == UPLOAD RESULT ===
|
|
37
|
+
def _upload_result(self, df):
|
|
38
|
+
if isinstance(df, dd.DataFrame):
|
|
39
|
+
df = df.compute()
|
|
40
|
+
elif not isinstance(df, pd.DataFrame):
|
|
41
|
+
raise ValueError("The input must be a Pandas or Dask DataFrame")
|
|
42
|
+
|
|
43
|
+
tmp_file_name = TempfileUtils.get_temp_file_name(
|
|
44
|
+
self.request.service_name, self.request.token, "result_", ".csv")
|
|
45
|
+
|
|
46
|
+
df.to_csv(tmp_file_name, index=False)
|
|
47
|
+
|
|
48
|
+
log(f"{self.request.service_name}: Upload Result File {tmp_file_name}")
|
|
49
|
+
self.task_manager.service_upload_result(self, tmp_file_name)
|
|
50
|
+
|
|
51
|
+
return tmp_file_name
|
|
52
|
+
|
|
53
|
+
# == UPLOAD REPORT ===
|
|
54
|
+
|
|
55
|
+
def _upload_report(self, report_name: str, report_json: str):
|
|
56
|
+
adapter_id = get_adapter_id(self.request.response_api)
|
|
57
|
+
service_name = self.request.service_name
|
|
58
|
+
token = self.request.token
|
|
59
|
+
|
|
60
|
+
# important need to recursive and convert numbers to str
|
|
61
|
+
report_json = deep_convert_numbers_to_strings(
|
|
62
|
+
report_json)
|
|
63
|
+
|
|
64
|
+
json_string = json.dumps(report_json)
|
|
65
|
+
|
|
66
|
+
report_name = f"data_{report_name}_no_index"
|
|
67
|
+
|
|
68
|
+
df = pd.DataFrame({
|
|
69
|
+
'customer': [adapter_id],
|
|
70
|
+
report_name: [json_string]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
report_csv_path = TempfileUtils.get_temp_file_name(
|
|
74
|
+
service_name, token, f"report_{report_name}_", '.csv')
|
|
75
|
+
|
|
76
|
+
df.to_csv(report_csv_path, index=False)
|
|
77
|
+
self.task_manager.service_upload_report(self, report_csv_path)
|
|
78
|
+
# =========
|
|
79
|
+
|
|
80
|
+
def _exit(self):
|
|
81
|
+
self.task_manager.service_exit(self)
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def on_start(self):
|
|
85
|
+
"""on sstart"""
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def on_data_input(self, req):
|
|
89
|
+
"""on_data_input"""
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def on_destroy(self):
|
|
93
|
+
"""on_destroy"""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def on_terminate(self):
|
|
97
|
+
"""on_terminate"""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def get_status(self):
|
|
101
|
+
"""get_status"""
|