pam-python 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- pam/api.py +42 -0
- pam/cli.py +107 -0
- pam/interface_task_manager.py +30 -0
- pam/models/__init__.py +0 -0
- pam/models/request_command.py +138 -0
- pam/server.py +170 -0
- pam/service.py +101 -0
- pam/task_manager.py +141 -0
- pam/temp_file_utils.py +75 -0
- pam/templates/buildcmd/pamb +31 -0
- pam/templates/buildcmd/pamb-base.sh +20 -0
- pam/templates/docker/Dockerfile +17 -0
- pam/templates/init/main.tmpl +11 -0
- pam/templates/service/ServiceClass.tmpl +48 -0
- pam/templates/service/functions.tmpl +22 -0
- pam/templates/service/service.test.tmpl +83 -0
- pam/templates/service/service.yaml +2 -0
- pam/tester_task.py +60 -0
- pam/utils.py +38 -0
- pam_python-0.1.0.dist-info/LICENSE.txt +17 -0
- pam_python-0.1.0.dist-info/METADATA +29 -0
- pam_python-0.1.0.dist-info/RECORD +26 -0
- pam_python-0.1.0.dist-info/WHEEL +5 -0
- pam_python-0.1.0.dist-info/entry_points.txt +2 -0
- pam_python-0.1.0.dist-info/top_level.txt +1 -0
pam/__init__.py
ADDED
|
File without changes
|
pam/api.py
ADDED
|
@@ -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
|
pam/cli.py
ADDED
|
@@ -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
|
pam/models/__init__.py
ADDED
|
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
|
pam/server.py
ADDED
|
@@ -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)
|
pam/service.py
ADDED
|
@@ -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"""
|
pam/task_manager.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from pam.utils import log
|
|
6
|
+
from pam.api import API
|
|
7
|
+
from pam.service import Service
|
|
8
|
+
from pam.models.request_command import RequestCommand
|
|
9
|
+
from pam.interface_task_manager import ITaskManager
|
|
10
|
+
from pam.temp_file_utils import TempfileUtils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ServiceHolder:
|
|
14
|
+
def __init__(self, service: Service):
|
|
15
|
+
self.service = service
|
|
16
|
+
self.last_activity = datetime.now()
|
|
17
|
+
|
|
18
|
+
def update_timestamp(self):
|
|
19
|
+
self.last_activity = datetime.now()
|
|
20
|
+
|
|
21
|
+
def has_timed_out(self, timeout=2):
|
|
22
|
+
# ตรวจสอบว่า service นี้ไม่เคลื่อนไหวเกินกว่า timeout ชั่วโมงหรือไม่
|
|
23
|
+
return datetime.now() - self.last_activity > timedelta(hours=timeout)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskManager(ITaskManager):
|
|
27
|
+
"""
|
|
28
|
+
Manage service thread
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, server):
|
|
32
|
+
self.services: Dict[str, ServiceHolder] = {}
|
|
33
|
+
self.api = API()
|
|
34
|
+
self.server = server
|
|
35
|
+
self.thread_lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
def _add_service(self, token, service):
|
|
38
|
+
with self.thread_lock:
|
|
39
|
+
self.services[token] = ServiceHolder(service)
|
|
40
|
+
|
|
41
|
+
def _get_service_holder(self, token):
|
|
42
|
+
with self.thread_lock:
|
|
43
|
+
return self.services.get(token)
|
|
44
|
+
|
|
45
|
+
def _update_service(self, token):
|
|
46
|
+
with self.thread_lock:
|
|
47
|
+
if token in self.services:
|
|
48
|
+
self.services[token].update_timestamp()
|
|
49
|
+
|
|
50
|
+
def _check_timeout(self, token, timeout=2):
|
|
51
|
+
with self.thread_lock:
|
|
52
|
+
if token in self.services:
|
|
53
|
+
return self.services[token].has_timed_out(timeout)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def _remove_service(self, token):
|
|
57
|
+
with self.thread_lock:
|
|
58
|
+
if token in self.services:
|
|
59
|
+
service_holder = self.services[token]
|
|
60
|
+
log(f"Service Exit: {
|
|
61
|
+
service_holder.service.request.service_name}, {service_holder.service.request.token}")
|
|
62
|
+
service_holder.service.on_destroy()
|
|
63
|
+
del self.services[token]
|
|
64
|
+
|
|
65
|
+
def start_service_monitoring_schedul(self):
|
|
66
|
+
schedule_thread = threading.Thread(target=self._monitor_services)
|
|
67
|
+
schedule_thread.daemon = True
|
|
68
|
+
schedule_thread.start()
|
|
69
|
+
|
|
70
|
+
def _monitor_services(self, interval=600, timeout=2):
|
|
71
|
+
while True:
|
|
72
|
+
time.sleep(interval)
|
|
73
|
+
log("Service Monitor running.")
|
|
74
|
+
tokens_to_remove = [
|
|
75
|
+
token for token in self.services if self._check_timeout(token, timeout)
|
|
76
|
+
]
|
|
77
|
+
log(f"Found: {len(tokens_to_remove)} services timeout.")
|
|
78
|
+
for token in tokens_to_remove:
|
|
79
|
+
log(f"Service {token} has timed out. Removing...")
|
|
80
|
+
self._remove_service(token)
|
|
81
|
+
|
|
82
|
+
def on_dataset_input(self, req: RequestCommand):
|
|
83
|
+
"""
|
|
84
|
+
handle cmd dataset
|
|
85
|
+
"""
|
|
86
|
+
service_holder = self._get_service_holder(req.token)
|
|
87
|
+
if service_holder is not None:
|
|
88
|
+
service_holder.update_timestamp()
|
|
89
|
+
service_holder.service.on_data_input(req)
|
|
90
|
+
|
|
91
|
+
def start_service(self, service_class, req: RequestCommand, service_name):
|
|
92
|
+
"""
|
|
93
|
+
Start new service
|
|
94
|
+
"""
|
|
95
|
+
log(f"Start Services {service_name}, Token: {req.token}")
|
|
96
|
+
service_instance = service_class(self, req)
|
|
97
|
+
service_instance.on_start()
|
|
98
|
+
self._add_service(req.token, service_instance)
|
|
99
|
+
|
|
100
|
+
def terminate_service(self, token):
|
|
101
|
+
"""
|
|
102
|
+
stop service that started from a token
|
|
103
|
+
"""
|
|
104
|
+
service_holder = self._get_service_holder(token)
|
|
105
|
+
if service_holder is not None:
|
|
106
|
+
service_holder.service.on_terminate()
|
|
107
|
+
self._remove_service(token)
|
|
108
|
+
|
|
109
|
+
# ======= Service Callback function ======
|
|
110
|
+
def service_request_data(self, service: Service, page):
|
|
111
|
+
endpoint = service.request.data_request_api
|
|
112
|
+
token = service.request.token
|
|
113
|
+
|
|
114
|
+
json_data = {"page": page, "token": token}
|
|
115
|
+
|
|
116
|
+
log(f"Request Data to: {endpoint}, page: {page}, token={token}")
|
|
117
|
+
http_thread = threading.Thread(
|
|
118
|
+
target=self.api.http_post, args=(endpoint, json_data, ))
|
|
119
|
+
http_thread.start()
|
|
120
|
+
http_thread.join()
|
|
121
|
+
|
|
122
|
+
def service_upload_result(self, service: Service, file_path):
|
|
123
|
+
endpoint = service.request.response_api
|
|
124
|
+
|
|
125
|
+
log(f"Upload Data to: {endpoint}")
|
|
126
|
+
http_thread = threading.Thread(
|
|
127
|
+
target=self.api.http_upload, args=(endpoint, file_path, ))
|
|
128
|
+
http_thread.start()
|
|
129
|
+
http_thread.join()
|
|
130
|
+
|
|
131
|
+
def service_upload_report(self, service: Service, file_path):
|
|
132
|
+
endpoint = service.request.response_api
|
|
133
|
+
|
|
134
|
+
log(f"Upload Report to: {endpoint}")
|
|
135
|
+
http_thread = threading.Thread(
|
|
136
|
+
target=self.api.http_upload, args=(endpoint, file_path, ))
|
|
137
|
+
http_thread.start()
|
|
138
|
+
http_thread.join()
|
|
139
|
+
|
|
140
|
+
def service_exit(self, service: Service):
|
|
141
|
+
self._remove_service(service.request.token)
|
pam/temp_file_utils.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
import shutil
|
|
4
|
+
import uuid
|
|
5
|
+
from pam.utils import log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def clean_old_folders(days: int):
|
|
9
|
+
|
|
10
|
+
base_folder = Path(TempfileUtils.temp_datasource_path)
|
|
11
|
+
|
|
12
|
+
if not base_folder.exists():
|
|
13
|
+
log("No temp dir to clean")
|
|
14
|
+
return
|
|
15
|
+
elif base_folder.is_file():
|
|
16
|
+
log("Something wrong, temp dir is not folder. Plugin will delete it.")
|
|
17
|
+
base_folder.unlink()
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
21
|
+
|
|
22
|
+
# Iterate through all subfolders in the base_folder
|
|
23
|
+
for folder in base_folder.iterdir():
|
|
24
|
+
if folder.is_dir():
|
|
25
|
+
try:
|
|
26
|
+
# Extract the date from the folder name
|
|
27
|
+
folder_date = datetime.strptime(folder.name, "%Y_%m_%d")
|
|
28
|
+
|
|
29
|
+
# If the folder is older than the cutoff date, remove it
|
|
30
|
+
if folder_date < cutoff_date:
|
|
31
|
+
shutil.rmtree(folder)
|
|
32
|
+
log(f"Removed folder: {folder}")
|
|
33
|
+
except ValueError:
|
|
34
|
+
# If folder name doesn't match the expected format, skip it
|
|
35
|
+
log(f"""Skipping folder: {
|
|
36
|
+
folder} (unexpected name format)""")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TempfileUtils:
|
|
40
|
+
|
|
41
|
+
temp_base_path = "/app/data"
|
|
42
|
+
temp_datasource_path = "/app/data/data_sources"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def clean_temp():
|
|
46
|
+
clean_old_folders(10)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get_temp_path(service_name, token):
|
|
50
|
+
date_path = datetime.now().strftime("%Y_%m_%d")
|
|
51
|
+
temp_dir = f"""{TempfileUtils.temp_datasource_path}/{date_path}/{service_name}/{
|
|
52
|
+
token}"""
|
|
53
|
+
folder_path = Path(temp_dir)
|
|
54
|
+
folder_path.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
return temp_dir
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def get_temp_file_name(service_name, token, prefix="", extension=""):
|
|
59
|
+
unique_filename = uuid.uuid1().hex
|
|
60
|
+
|
|
61
|
+
if prefix:
|
|
62
|
+
if prefix.endswith("_"):
|
|
63
|
+
unique_filename = f"{prefix}{unique_filename}"
|
|
64
|
+
else:
|
|
65
|
+
unique_filename = f"{prefix}_{unique_filename}"
|
|
66
|
+
|
|
67
|
+
if extension:
|
|
68
|
+
if extension.startswith("."):
|
|
69
|
+
unique_filename = f"{unique_filename}{extension}"
|
|
70
|
+
else:
|
|
71
|
+
unique_filename = f"{unique_filename}.{extension}"
|
|
72
|
+
|
|
73
|
+
temp_path = TempfileUtils.get_temp_path(service_name, token)
|
|
74
|
+
full_path = Path(temp_path) / unique_filename
|
|
75
|
+
return full_path
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
CMD=$1
|
|
4
|
+
APP_VERSION=$2
|
|
5
|
+
IMAGE=$3
|
|
6
|
+
|
|
7
|
+
function print_howto() {
|
|
8
|
+
echo "How to use : pamb CMD APP_VERSION TIMESTAMP NAMESPACE OTHER_PARAM1 OTHER_PARAM2 (eg. pamb build_pam 6.0 201906011030 stg)"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if [ "$CMD" = "" ]; then
|
|
12
|
+
echo -e "CMD cannot be blank"
|
|
13
|
+
print_howto
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [ "$APP_VERSION" = "" ]; then
|
|
18
|
+
echo -e "APP_VERSION cannot be blank"
|
|
19
|
+
print_howto
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [ "$IMAGE" = "" ]; then
|
|
24
|
+
echo -e "IMAGE cannot be blank"
|
|
25
|
+
print_howto
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
source ./pamb-base.sh
|
|
30
|
+
|
|
31
|
+
eval ${CMD}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
function commit_version() {
|
|
4
|
+
local IMAGE=$1
|
|
5
|
+
local VERSION=$2
|
|
6
|
+
echo "docker push image : 3dsinteractive/$IMAGE:$VERSION"
|
|
7
|
+
docker login -u $DOCKER_USER -p $DOCKER_PASS
|
|
8
|
+
docker push 3dsinteractive/$IMAGE:$VERSION
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function prepare_project() {
|
|
12
|
+
git config --global url.ssh://git@bitbucket.org/.insteadOf https://bitbucket.org/
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function build_plugin() {
|
|
16
|
+
prepare_project
|
|
17
|
+
|
|
18
|
+
docker build -f Dockerfile -t 3dsinteractive/$IMAGE:$APP_VERSION .
|
|
19
|
+
commit_version $IMAGE $APP_VERSION
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
COPY . /app
|
|
4
|
+
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
|
|
7
|
+
ENV DATA_FOLDER=/app/data
|
|
8
|
+
RUN mkdir -p $DATA_FOLDER
|
|
9
|
+
|
|
10
|
+
RUN pip install --upgrade pip
|
|
11
|
+
RUN pip install -r requirements.txt
|
|
12
|
+
|
|
13
|
+
EXPOSE 8000
|
|
14
|
+
|
|
15
|
+
ENV PYTHONUNBUFFERED=1
|
|
16
|
+
|
|
17
|
+
CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "main:app"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from pam.service import Service
|
|
3
|
+
from pam.models.request_command import RequestCommand
|
|
4
|
+
from pam.utils import log
|
|
5
|
+
from functions import *
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class #CLASS_NAME#(Service):
|
|
9
|
+
|
|
10
|
+
def __init__(self, task_manager, req):
|
|
11
|
+
super().__init__(task_manager, req)
|
|
12
|
+
|
|
13
|
+
def on_start(self):
|
|
14
|
+
log("on_start")
|
|
15
|
+
self._request_data()
|
|
16
|
+
|
|
17
|
+
def on_data_input(self, req: RequestCommand):
|
|
18
|
+
log(f"on_data_input req.is_end = {req.is_end}")
|
|
19
|
+
|
|
20
|
+
dataframe = self.__process_data(req.input_files)
|
|
21
|
+
self._upload_result(dataframe)
|
|
22
|
+
|
|
23
|
+
if not req.is_end:
|
|
24
|
+
self._request_data(req.next)
|
|
25
|
+
else:
|
|
26
|
+
report_df = create_report()
|
|
27
|
+
report_name = "#CLASS_NAME#_report1"
|
|
28
|
+
self._upload_report(report_name, report_df)
|
|
29
|
+
|
|
30
|
+
self._exit()
|
|
31
|
+
|
|
32
|
+
def __process_data(self, input_files):
|
|
33
|
+
file_path = input_files[0]
|
|
34
|
+
df = load_csv_file(file_path)
|
|
35
|
+
df['finished'] = True
|
|
36
|
+
return df
|
|
37
|
+
|
|
38
|
+
def __create_report():
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def on_destroy(self):
|
|
42
|
+
log("on_destroy")
|
|
43
|
+
|
|
44
|
+
def on_terminate(self):
|
|
45
|
+
log("on_terminate")
|
|
46
|
+
|
|
47
|
+
def get_status(self):
|
|
48
|
+
return ""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
def load_csv_file(csv_file):
|
|
4
|
+
dtype_spec = {
|
|
5
|
+
'id': 'str'
|
|
6
|
+
}
|
|
7
|
+
df = pd.read_csv(csv_file, dtype=dtype_spec, parse_dates=['created_date'])
|
|
8
|
+
return df
|
|
9
|
+
|
|
10
|
+
def create_report():
|
|
11
|
+
report = {
|
|
12
|
+
"type": "TABLE",
|
|
13
|
+
"headers": ["test",
|
|
14
|
+
"column1",
|
|
15
|
+
"column2"
|
|
16
|
+
],
|
|
17
|
+
"rows": [
|
|
18
|
+
["test1", 1, 2],
|
|
19
|
+
["test2", 3, 4]
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
return report
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
from #CLASS_NAME#_service import #CLASS_NAME#
|
|
4
|
+
from pam.tester_task import TesterTask
|
|
5
|
+
from pam.models.request_command import RequestCommand
|
|
6
|
+
from faker import Faker
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import uuid
|
|
11
|
+
from pam.temp_file_utils import TempfileUtils
|
|
12
|
+
|
|
13
|
+
def __create_mock_csv(page):
|
|
14
|
+
fake = Faker()
|
|
15
|
+
data = {
|
|
16
|
+
"id": range(1, 11), # 10 rows
|
|
17
|
+
"name": [fake.name() for _ in range(10)],
|
|
18
|
+
"age": [fake.random_int(min=18, max=65) for _ in range(10)],
|
|
19
|
+
"email": [fake.email() for _ in range(10)],
|
|
20
|
+
"created_date": [fake.date_time_this_year() for _ in range(10)]
|
|
21
|
+
}
|
|
22
|
+
file_name = f"{mock_dir}/mock_data_faker_{page}.csv"
|
|
23
|
+
df = pd.DataFrame(data)
|
|
24
|
+
df.to_csv(file_name, index=False)
|
|
25
|
+
return file_name
|
|
26
|
+
|
|
27
|
+
def can_convert_to_int(x):
|
|
28
|
+
try:
|
|
29
|
+
int(x)
|
|
30
|
+
return True
|
|
31
|
+
except (ValueError, TypeError):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def on_request_data(page) -> Tuple[List[str], bool, str]:
|
|
35
|
+
page_num = 1
|
|
36
|
+
if can_convert_to_int(page):
|
|
37
|
+
page_num = int(page)
|
|
38
|
+
|
|
39
|
+
next_page = str(page_num + 1)
|
|
40
|
+
is_end = False
|
|
41
|
+
if page_num == MAXPAGE:
|
|
42
|
+
is_end = True
|
|
43
|
+
next_page = ""
|
|
44
|
+
|
|
45
|
+
mock_csv = __create_mock_csv(page_num)
|
|
46
|
+
|
|
47
|
+
return [mock_csv], is_end, next_page
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def on_upload_report(report_file):
|
|
51
|
+
print(f"Uploaded Report {report_file}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def on_upload_result(result_file):
|
|
55
|
+
print(f"Uploaded {result_file}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#---- Constants ----
|
|
60
|
+
MAXPAGE=3
|
|
61
|
+
mock_dir = "./TEMP_TEST_DATA"
|
|
62
|
+
TempfileUtils.temp_base_path = f"{mock_dir}/app/data"
|
|
63
|
+
TempfileUtils.temp_datasource_path = f"{mock_dir}/app/data/data_sources"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#---- Create Mock Dir ----
|
|
67
|
+
if os.path.exists(mock_dir):
|
|
68
|
+
shutil.rmtree(mock_dir)
|
|
69
|
+
os.makedirs(mock_dir)
|
|
70
|
+
|
|
71
|
+
#---- Create Tester Task ----
|
|
72
|
+
testerTask = TesterTask()
|
|
73
|
+
testerTask.set_on_upload_result(on_upload_result)
|
|
74
|
+
testerTask.set_on_request_data(on_request_data)
|
|
75
|
+
testerTask.set_on_upload_report(on_upload_report)
|
|
76
|
+
|
|
77
|
+
#---- Create Request Command ----
|
|
78
|
+
token=unique_id = uuid.uuid4()
|
|
79
|
+
req = RequestCommand(token, "start", "-", "-", None, None, None, '#CLASS_NAME#')
|
|
80
|
+
#CLASS_NAME# = #CLASS_NAME#(testerTask, req)
|
|
81
|
+
|
|
82
|
+
#---- Start Test ----
|
|
83
|
+
testerTask.test_service(#CLASS_NAME#)
|
pam/tester_task.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Callable, Tuple, List
|
|
2
|
+
from pam.models.request_command import RequestCommand
|
|
3
|
+
from pam.interface_task_manager import ITaskManager
|
|
4
|
+
from pam.service import Service
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
RequestDataCallbackType = Callable[[str], Tuple[List[str], bool, str]]
|
|
8
|
+
|
|
9
|
+
UploadResultCallbackType = Callable[[str], None]
|
|
10
|
+
|
|
11
|
+
UploadReportCallbackType = Callable[[str], None]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TesterTask(ITaskManager):
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.request_data_callback: RequestDataCallbackType = None
|
|
18
|
+
self.upload_result_calllback: UploadResultCallbackType = None
|
|
19
|
+
self.upload_report_calllback: UploadReportCallbackType = None
|
|
20
|
+
|
|
21
|
+
def set_on_request_data(self, request_data_callback: RequestDataCallbackType):
|
|
22
|
+
self.request_data_callback = request_data_callback
|
|
23
|
+
|
|
24
|
+
def set_on_upload_result(self, upload_result_calllback: UploadResultCallbackType):
|
|
25
|
+
self.upload_result_calllback = upload_result_calllback
|
|
26
|
+
|
|
27
|
+
def set_on_upload_report(self, upload_report_calllback: UploadReportCallbackType):
|
|
28
|
+
self.upload_report_calllback = upload_report_calllback
|
|
29
|
+
|
|
30
|
+
def test_service(self, service):
|
|
31
|
+
service.on_start()
|
|
32
|
+
|
|
33
|
+
# -----
|
|
34
|
+
|
|
35
|
+
def on_dataset_input(self, req: RequestCommand):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
def start_service(self, service_class, req: RequestCommand, service_name):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def terminate_service(self, token):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def service_exit(self, service_class):
|
|
45
|
+
print(f"{service_class.request.service_name} Exit.")
|
|
46
|
+
|
|
47
|
+
def service_request_data(self, service: Service, page):
|
|
48
|
+
if self.request_data_callback is not None:
|
|
49
|
+
files, is_end, next_page = self.request_data_callback(page)
|
|
50
|
+
req = RequestCommand(service.request.token, "dataset", "",
|
|
51
|
+
"", is_end, next_page, files, service.request.service_name)
|
|
52
|
+
service.on_data_input(req)
|
|
53
|
+
|
|
54
|
+
def service_upload_result(self, service: Service, file_path):
|
|
55
|
+
if self.upload_result_calllback is not None:
|
|
56
|
+
self.upload_result_calllback(file_path)
|
|
57
|
+
|
|
58
|
+
def service_upload_report(self, service: Service, file_path):
|
|
59
|
+
if self.upload_report_calllback is not None:
|
|
60
|
+
self.upload_report_calllback(file_path)
|
pam/utils.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import re
|
|
5
|
+
import pytz
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_adapter_id(url: str) -> str:
|
|
9
|
+
pattern = r'/api/dataadapter/(\w+)/response'
|
|
10
|
+
match = re.search(pattern, url)
|
|
11
|
+
if match:
|
|
12
|
+
return match.group(1)
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def deep_convert_numbers_to_strings(data):
|
|
17
|
+
if isinstance(data, dict):
|
|
18
|
+
return {key: deep_convert_numbers_to_strings(
|
|
19
|
+
value) for key, value in data.items()}
|
|
20
|
+
|
|
21
|
+
if isinstance(data, list):
|
|
22
|
+
return [deep_convert_numbers_to_strings(item) for item in data]
|
|
23
|
+
|
|
24
|
+
if isinstance(data, tuple):
|
|
25
|
+
return tuple(deep_convert_numbers_to_strings(item) for item in data)
|
|
26
|
+
|
|
27
|
+
if isinstance(data, (int, float)):
|
|
28
|
+
return str(data)
|
|
29
|
+
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log(msg: str):
|
|
34
|
+
utc_dt = datetime.now(pytz.utc) # Get current time in UTC
|
|
35
|
+
tz = pytz.timezone('Asia/Bangkok')
|
|
36
|
+
bangkok_time = utc_dt.astimezone(tz) # Convert UTC time to Bangkok time
|
|
37
|
+
date_str = bangkok_time.strftime("%m/%d/%Y %H:%M:%S")
|
|
38
|
+
print(f"[{date_str}]: {msg}")
|
|
@@ -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.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
pam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pam/api.py,sha256=pNY1qTSTalCk8nzXRZ845gXGZwpK2iNwOtjZnEZdEDY,1472
|
|
3
|
+
pam/cli.py,sha256=dqfGXMJGBqt1HJ-ZbxHLndPa9hV_UaHgu0SvxwFAZrQ,3671
|
|
4
|
+
pam/interface_task_manager.py,sha256=6DGAUcf6IxNIYIZZcFRjZh6NynSm5BWZgrJP0GdCrdQ,717
|
|
5
|
+
pam/server.py,sha256=hvp-ZQcp2zJTFxOeVLEQg3f9aZgBLwTTSgfeRV94J6A,5374
|
|
6
|
+
pam/service.py,sha256=oavcz_ggkKTk7RgvGNwwpbHw75nqg2b5fTxsMMKkKwE,2803
|
|
7
|
+
pam/task_manager.py,sha256=6ChMLTLp9GCxKUcJAZzHtb9l-5K83mc_3XRwtxD-Esk,5011
|
|
8
|
+
pam/temp_file_utils.py,sha256=pvI8klHQqnH3-GL_7vGPHKtvZRyEC1PBGqbxrqwi05Y,2434
|
|
9
|
+
pam/tester_task.py,sha256=S-fxXMH5FMaeJVv_FAxHiJCFb3cStSQoRrRqxK8Yutk,2191
|
|
10
|
+
pam/utils.py,sha256=nOndtbbdlHx5vfUZJpO-qR59eWvvF3xoCBOQSqKckiM,1029
|
|
11
|
+
pam/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
pam/models/request_command.py,sha256=KnARL5sVbIr_xUcF0EMMxCDHQRX57M2gUcRLPKm2Vjs,4239
|
|
13
|
+
pam/templates/buildcmd/pamb,sha256=58GCN59ysvHGNhPvTD3nkF4aLg-kM1WfWmH7CL3eQVo,530
|
|
14
|
+
pam/templates/buildcmd/pamb-base.sh,sha256=Q0DkbeHtPx33PIjc68EeI7ZIwBOLlwcSmPhpxALXxVY,518
|
|
15
|
+
pam/templates/docker/Dockerfile,sha256=d266YB4XYRJEM3mtU9uSQcL_rsCZojoA04fW8OqovPU,268
|
|
16
|
+
pam/templates/init/main.tmpl,sha256=v0n0H4YNrRPPPZlbzdL2vpLwQgCMgJQ6Cm7nR6oo4qU,192
|
|
17
|
+
pam/templates/service/ServiceClass.tmpl,sha256=M30LOdAIBfnLBLhBO--3DWzUz1EWwtcRHS-I-vUBvZE,1188
|
|
18
|
+
pam/templates/service/functions.tmpl,sha256=RMG8_PsKyhHgBMeXBfCavRlUW00rqNoo6QKfG3pZgqU,470
|
|
19
|
+
pam/templates/service/service.test.tmpl,sha256=YmdEnVMmdpwM8iT1nXF1fSKOjg0XBWXfbXRwcecxi0s,2198
|
|
20
|
+
pam/templates/service/service.yaml,sha256=S9r6MeysjlBZVr-6OykrwFO5RJqrhsoLr6R8FQtD8gg,44
|
|
21
|
+
pam_python-0.1.0.dist-info/LICENSE.txt,sha256=yeHD2-gzrkkET2ZkMe22WyUGpDjlwTrqA4POb0FoZi4,1138
|
|
22
|
+
pam_python-0.1.0.dist-info/METADATA,sha256=nnkU4wLWA0kRda746Ytv0_s5AHIdYbUvMYsT60nHUEg,1102
|
|
23
|
+
pam_python-0.1.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
24
|
+
pam_python-0.1.0.dist-info/entry_points.txt,sha256=xqYpsIqvNlO0j8elZc02bDwR-12dLK9FU2B1ACsLYxw,37
|
|
25
|
+
pam_python-0.1.0.dist-info/top_level.txt,sha256=0EOjbyc3hQyzjhn6iyMgsEseqA66Xz0p27iBN7G7W1w,4
|
|
26
|
+
pam_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pam
|