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.
Files changed (31) hide show
  1. pam_python-0.1.0/LICENSE.txt +17 -0
  2. pam_python-0.1.0/PKG-INFO +29 -0
  3. pam_python-0.1.0/README.md +3 -0
  4. pam_python-0.1.0/pam/__init__.py +0 -0
  5. pam_python-0.1.0/pam/api.py +42 -0
  6. pam_python-0.1.0/pam/cli.py +107 -0
  7. pam_python-0.1.0/pam/interface_task_manager.py +30 -0
  8. pam_python-0.1.0/pam/models/__init__.py +0 -0
  9. pam_python-0.1.0/pam/models/request_command.py +138 -0
  10. pam_python-0.1.0/pam/server.py +170 -0
  11. pam_python-0.1.0/pam/service.py +101 -0
  12. pam_python-0.1.0/pam/task_manager.py +141 -0
  13. pam_python-0.1.0/pam/temp_file_utils.py +75 -0
  14. pam_python-0.1.0/pam/templates/buildcmd/pamb +31 -0
  15. pam_python-0.1.0/pam/templates/buildcmd/pamb-base.sh +20 -0
  16. pam_python-0.1.0/pam/templates/docker/Dockerfile +17 -0
  17. pam_python-0.1.0/pam/templates/init/main.tmpl +11 -0
  18. pam_python-0.1.0/pam/templates/service/ServiceClass.tmpl +48 -0
  19. pam_python-0.1.0/pam/templates/service/functions.tmpl +22 -0
  20. pam_python-0.1.0/pam/templates/service/service.test.tmpl +83 -0
  21. pam_python-0.1.0/pam/templates/service/service.yaml +2 -0
  22. pam_python-0.1.0/pam/tester_task.py +60 -0
  23. pam_python-0.1.0/pam/utils.py +38 -0
  24. pam_python-0.1.0/pam_python.egg-info/PKG-INFO +29 -0
  25. pam_python-0.1.0/pam_python.egg-info/SOURCES.txt +29 -0
  26. pam_python-0.1.0/pam_python.egg-info/dependency_links.txt +1 -0
  27. pam_python-0.1.0/pam_python.egg-info/entry_points.txt +2 -0
  28. pam_python-0.1.0/pam_python.egg-info/requires.txt +7 -0
  29. pam_python-0.1.0/pam_python.egg-info/top_level.txt +1 -0
  30. pam_python-0.1.0/setup.cfg +4 -0
  31. 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.
@@ -0,0 +1,3 @@
1
+ ## License
2
+
3
+ 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"""