pam-python 0.1.0__tar.gz → 0.1.2__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.2/PKG-INFO +126 -0
- pam_python-0.1.2/README.md +99 -0
- pam_python-0.1.2/pam/api.py +53 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/cli.py +37 -10
- pam_python-0.1.2/pam/models/request_command.py +160 -0
- pam_python-0.1.2/pam/server.py +180 -0
- pam_python-0.1.2/pam/service.py +124 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/task_manager.py +56 -34
- pam_python-0.1.2/pam/temp_file_utils.py +100 -0
- pam_python-0.1.2/pam/templates/init/dockerignore.tmpl +4 -0
- pam_python-0.1.2/pam/templates/init/gitignore.tmpl +178 -0
- pam_python-0.1.2/pam/templates/init/pylintrc.tmpl +2 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/service.test.tmpl +35 -16
- pam_python-0.1.0/pam/templates/service/ServiceClass.tmpl → pam_python-0.1.2/pam/templates/service/service_class.tmpl +9 -7
- pam_python-0.1.2/pam/tester_task.py +118 -0
- pam_python-0.1.2/pam/utils.py +60 -0
- pam_python-0.1.2/pam_python.egg-info/PKG-INFO +126 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/SOURCES.txt +4 -1
- {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/requires.txt +1 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/setup.py +4 -3
- pam_python-0.1.0/PKG-INFO +0 -29
- pam_python-0.1.0/README.md +0 -3
- pam_python-0.1.0/pam/api.py +0 -42
- pam_python-0.1.0/pam/models/request_command.py +0 -138
- pam_python-0.1.0/pam/server.py +0 -170
- pam_python-0.1.0/pam/service.py +0 -101
- pam_python-0.1.0/pam/temp_file_utils.py +0 -75
- pam_python-0.1.0/pam/tester_task.py +0 -60
- pam_python-0.1.0/pam/utils.py +0 -38
- pam_python-0.1.0/pam_python.egg-info/PKG-INFO +0 -29
- {pam_python-0.1.0 → pam_python-0.1.2}/LICENSE.txt +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/__init__.py +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/interface_task_manager.py +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/models/__init__.py +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/docker/Dockerfile +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/functions.tmpl +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/service.yaml +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.1.0 → pam_python-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pam-python
|
|
3
|
+
Version: 0.1.2
|
|
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
|
+
Requires-Dist: requests>=2.32.3
|
|
27
|
+
|
|
28
|
+
# pam-python
|
|
29
|
+
|
|
30
|
+
**pam-python** is a framework designed to simplify the creation of data plugins for PAM Real CDP. These data plugins allow you to extend the capabilities of PAM CDP to process data in a fully customizable way. For example, you can create RFM segments, run analytics such as finding top spenders, and more.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Streamlined creation of data plugins for PAM Real CDP.
|
|
35
|
+
- Scaffolding for project and service structures.
|
|
36
|
+
- Built-in templates for rapid development.
|
|
37
|
+
- Command-line utility for managing projects and services.
|
|
38
|
+
- Easy-to-run testing for individual services.
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
### Prerequisites
|
|
43
|
+
|
|
44
|
+
- Python 3.6 or later
|
|
45
|
+
- pip for package management
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### 1. Create a new folder for your project
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mkdir my_data_plugin
|
|
53
|
+
cd my_data_plugin
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Set up a Python virtual environment
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python3 -m venv venv
|
|
60
|
+
source venv/bin/activate
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Install pam-python
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install pam-python
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> Once installed, the `pam` command-line utility will be available.
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### 1. Initialize a New Project
|
|
74
|
+
|
|
75
|
+
To initialize a new data plugin project, run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pam init
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> This command will create the necessary files and structure for your project.
|
|
82
|
+
|
|
83
|
+
### 2. Add a New Service
|
|
84
|
+
|
|
85
|
+
Within your project, you can create multiple services. To add a new service, use:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pam new service <service_name>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> This will create a folder named `<service_name>` containing the following:
|
|
92
|
+
> Basic template files to help you start developing your service.
|
|
93
|
+
|
|
94
|
+
### 3. Test a Service
|
|
95
|
+
|
|
96
|
+
To test a specific service, use:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pam test <service_name>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
> This command will automatically run all test files (\*.test.py) associated with the specified service.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Example Workflow
|
|
107
|
+
|
|
108
|
+
### 1. Initialize the project
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pam init
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. Create a new service
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pam new service RFMAnalysis
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 3.Customize the service by editing the generated templates in RFMAnalysis
|
|
121
|
+
|
|
122
|
+
### 4. Test the service
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pam test RFMAnalysis
|
|
126
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pam-python
|
|
2
|
+
|
|
3
|
+
**pam-python** is a framework designed to simplify the creation of data plugins for PAM Real CDP. These data plugins allow you to extend the capabilities of PAM CDP to process data in a fully customizable way. For example, you can create RFM segments, run analytics such as finding top spenders, and more.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Streamlined creation of data plugins for PAM Real CDP.
|
|
8
|
+
- Scaffolding for project and service structures.
|
|
9
|
+
- Built-in templates for rapid development.
|
|
10
|
+
- Command-line utility for managing projects and services.
|
|
11
|
+
- Easy-to-run testing for individual services.
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
### Prerequisites
|
|
16
|
+
|
|
17
|
+
- Python 3.6 or later
|
|
18
|
+
- pip for package management
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### 1. Create a new folder for your project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
mkdir my_data_plugin
|
|
26
|
+
cd my_data_plugin
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Set up a Python virtual environment
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python3 -m venv venv
|
|
33
|
+
source venv/bin/activate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 3. Install pam-python
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pam-python
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Once installed, the `pam` command-line utility will be available.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### 1. Initialize a New Project
|
|
47
|
+
|
|
48
|
+
To initialize a new data plugin project, run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pam init
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> This command will create the necessary files and structure for your project.
|
|
55
|
+
|
|
56
|
+
### 2. Add a New Service
|
|
57
|
+
|
|
58
|
+
Within your project, you can create multiple services. To add a new service, use:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pam new service <service_name>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> This will create a folder named `<service_name>` containing the following:
|
|
65
|
+
> Basic template files to help you start developing your service.
|
|
66
|
+
|
|
67
|
+
### 3. Test a Service
|
|
68
|
+
|
|
69
|
+
To test a specific service, use:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pam test <service_name>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> This command will automatically run all test files (\*.test.py) associated with the specified service.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Example Workflow
|
|
80
|
+
|
|
81
|
+
### 1. Initialize the project
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pam init
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Create a new service
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pam new service RFMAnalysis
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3.Customize the service by editing the generated templates in RFMAnalysis
|
|
94
|
+
|
|
95
|
+
### 4. Test the service
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pam test RFMAnalysis
|
|
99
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import requests
|
|
3
|
+
from pam.utils import log
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class API:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.session = requests.Session() # Use a session for connection reuse
|
|
9
|
+
|
|
10
|
+
def http_post(self, url: str, data: dict) -> requests.Response | None:
|
|
11
|
+
"""
|
|
12
|
+
Sends an HTTP POST request to the specified URL with the given data as JSON.
|
|
13
|
+
|
|
14
|
+
:param url: The URL to send the POST request to.
|
|
15
|
+
:param data: A dictionary to be used as the JSON body of the POST request.
|
|
16
|
+
:return: The response from the server, or None if an error occurred.
|
|
17
|
+
"""
|
|
18
|
+
headers = {'Content-Type': 'application/json'}
|
|
19
|
+
try:
|
|
20
|
+
response = self.session.post(
|
|
21
|
+
url, json=data, timeout=30, headers=headers
|
|
22
|
+
)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
return response
|
|
25
|
+
except requests.RequestException as e:
|
|
26
|
+
log(f"HTTP POST request failed. URL: {url}, Error: {e}")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def http_upload(self, url: str, file_path: str) -> requests.Response | None:
|
|
30
|
+
"""
|
|
31
|
+
Uploads a file to the specified URL.
|
|
32
|
+
|
|
33
|
+
:param url: The URL to upload the file to.
|
|
34
|
+
:param file_path: The path to the file to be uploaded.
|
|
35
|
+
:return: The response from the server, or None if an error occurred.
|
|
36
|
+
"""
|
|
37
|
+
if not Path(file_path).is_file():
|
|
38
|
+
log(f"File does not exist: {file_path}")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
with open(file_path, 'rb') as file:
|
|
43
|
+
files = {'file': file}
|
|
44
|
+
response = self.session.post(url, files=files, timeout=300)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
return response
|
|
47
|
+
except requests.RequestException as e:
|
|
48
|
+
log(f"File upload failed. URL: {url}, File: {file_path}, Error: {e}")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def close(self):
|
|
52
|
+
"""Close the session."""
|
|
53
|
+
self.session.close()
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
-
import
|
|
3
|
+
import re
|
|
4
4
|
import shutil
|
|
5
5
|
import glob
|
|
6
6
|
import subprocess
|
|
7
|
+
import pkg_resources
|
|
7
8
|
|
|
8
9
|
def main():
|
|
9
10
|
args = sys.argv[1:]
|
|
@@ -16,8 +17,8 @@ def main():
|
|
|
16
17
|
if cmd == "init":
|
|
17
18
|
init_project()
|
|
18
19
|
elif cmd == "new":
|
|
19
|
-
|
|
20
|
-
if
|
|
20
|
+
create_type = args[1]
|
|
21
|
+
if create_type == "service":
|
|
21
22
|
name = args[2]
|
|
22
23
|
create_service(name)
|
|
23
24
|
elif cmd == "test":
|
|
@@ -45,7 +46,7 @@ def test_module(module_name):
|
|
|
45
46
|
# Run each test file
|
|
46
47
|
for test_file in test_files:
|
|
47
48
|
print(f"\nRunning tests in {test_file}...")
|
|
48
|
-
result = subprocess.run([sys.executable, test_file], capture_output=True, text=True)
|
|
49
|
+
result = subprocess.run([sys.executable, test_file], capture_output=True, text=True, check=True)
|
|
49
50
|
|
|
50
51
|
print("\nTest Output:")
|
|
51
52
|
print(result.stdout)
|
|
@@ -54,6 +55,24 @@ def test_module(module_name):
|
|
|
54
55
|
print(result.stderr)
|
|
55
56
|
print("-" * 40) # Separator for clarity
|
|
56
57
|
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def to_pascal_case(input_string: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Convert a string to PascalCase.
|
|
63
|
+
|
|
64
|
+
:param input_string: The string to convert.
|
|
65
|
+
:return: The string in PascalCase.
|
|
66
|
+
"""
|
|
67
|
+
# Split the string into words using non-alphanumeric characters as delimiters
|
|
68
|
+
words = re.split(r'\W+', input_string)
|
|
69
|
+
|
|
70
|
+
# Capitalize each word and join them
|
|
71
|
+
pascal_case = ''.join(word.capitalize() for word in words if word)
|
|
72
|
+
|
|
73
|
+
return pascal_case
|
|
74
|
+
|
|
75
|
+
|
|
57
76
|
def cpy(src, dest):
|
|
58
77
|
template_dir = pkg_resources.resource_filename("pam", "templates")
|
|
59
78
|
src_file = os.path.join(template_dir, src)
|
|
@@ -61,9 +80,10 @@ def cpy(src, dest):
|
|
|
61
80
|
|
|
62
81
|
def replace_template_content(service_name, file_name):
|
|
63
82
|
file_path = os.path.join(service_name, file_name)
|
|
64
|
-
with open(file_path, 'r+') as file:
|
|
83
|
+
with open(file_path, 'r+', encoding='utf-8') as file:
|
|
65
84
|
filedata = file.read()
|
|
66
|
-
updated_data = filedata.replace('#CLASS_NAME#', service_name)
|
|
85
|
+
updated_data = filedata.replace('#CLASS_NAME#', to_pascal_case(service_name))
|
|
86
|
+
updated_data = updated_data.replace('#MODULE_NAME#', service_name)
|
|
67
87
|
file.seek(0) # Move the file pointer to the beginning of the file
|
|
68
88
|
file.write(updated_data)
|
|
69
89
|
file.truncate() # Remove any leftover content after the replacement
|
|
@@ -76,15 +96,14 @@ def create_service(name):
|
|
|
76
96
|
else:
|
|
77
97
|
print("Cancelled.")
|
|
78
98
|
return
|
|
79
|
-
|
|
99
|
+
|
|
80
100
|
os.mkdir(name)
|
|
81
101
|
open(os.path.join(name, "__init__.py"), 'a', encoding='utf-8').close()
|
|
82
|
-
cpy("service/
|
|
102
|
+
cpy("service/service_class.tmpl", os.path.join(name, name+"_service.py") )
|
|
83
103
|
cpy("service/service.yaml", os.path.join(name, "service.yaml") )
|
|
84
104
|
cpy("service/functions.tmpl", os.path.join(name, "functions.py") )
|
|
85
105
|
cpy("service/service.test.tmpl", os.path.join(name, name+".test.py") )
|
|
86
106
|
|
|
87
|
-
|
|
88
107
|
replace_template_content(name, name+"_service.py")
|
|
89
108
|
replace_template_content(name, "service.yaml")
|
|
90
109
|
replace_template_content(name, name+".test.py")
|
|
@@ -92,16 +111,24 @@ def create_service(name):
|
|
|
92
111
|
print(f"Service {name} created.")
|
|
93
112
|
print(f"Run `pam test {name}` to run tests for the service.")
|
|
94
113
|
|
|
114
|
+
|
|
95
115
|
def init_project():
|
|
96
116
|
cpy("init/main.tmpl", "main.py")
|
|
97
117
|
cpy("docker/Dockerfile", "Dockerfile")
|
|
98
118
|
cpy("buildcmd/pamb", "pamb")
|
|
99
119
|
cpy("buildcmd/pamb-base.sh", "pamb-base.sh")
|
|
120
|
+
cpy("init/pylintrc.tmpl", ".pylintrc")
|
|
121
|
+
cpy("init/gitignore.tmpl", ".gitignore")
|
|
122
|
+
cpy("init/dockerignore.tmpl", ".dockerignore")
|
|
123
|
+
|
|
100
124
|
if not os.path.exists("requirements.txt"):
|
|
101
125
|
open("requirements.txt", 'a', encoding='utf-8').close()
|
|
102
126
|
|
|
103
127
|
with open("requirements.txt", "w", encoding='utf-8') as f:
|
|
104
128
|
subprocess.run(["pip", "freeze"], stdout=f, check=True)
|
|
105
129
|
|
|
130
|
+
if not os.path.exists("__init__.py"):
|
|
131
|
+
open("__init__.py", 'a', encoding='utf-8').close()
|
|
132
|
+
|
|
106
133
|
print("--- Welcome to PAM ---\n")
|
|
107
|
-
print("To create a new servive run\n`pam new service <service_name>`\n\n")
|
|
134
|
+
print("To create a new servive run\n`pam new service <service_name>`\n\n")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the RequestCommand class,
|
|
3
|
+
which wraps an HTTP request to simplify access to its parameters and files.
|
|
4
|
+
|
|
5
|
+
Classes:
|
|
6
|
+
RequestCommand: A dataclass that represents an HTTP request command
|
|
7
|
+
with various attributes and methods to process the request.
|
|
8
|
+
|
|
9
|
+
Methods:
|
|
10
|
+
is_start_command(self): Determine if this command is a start command.
|
|
11
|
+
is_dataset_command(self): Determine if this command is a dataset command.
|
|
12
|
+
__safe_str_to_bool(value): Safely convert a string to a boolean.
|
|
13
|
+
__extract_data_from_request(request: Request): Extract data from a Flask request object
|
|
14
|
+
based on its content type.
|
|
15
|
+
__allowed_file(filename): Check if the file has an allowed extension.
|
|
16
|
+
__extract_input_file(zip_file, service_name, token): Extract a zip file
|
|
17
|
+
into a temporary directory and return the file paths.
|
|
18
|
+
parse(request, service_name): Create a RequestCommand object from an HTTP request.
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
import zipfile
|
|
25
|
+
from flask import Request
|
|
26
|
+
from pam.utils import log
|
|
27
|
+
from pam.temp_file_utils import TempfileUtils
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class RequestCommand:
|
|
31
|
+
"""
|
|
32
|
+
Wraps an HTTP request to simplify access to its parameters and files.
|
|
33
|
+
"""
|
|
34
|
+
runtime_parameters: dict
|
|
35
|
+
token: str
|
|
36
|
+
cmd: str
|
|
37
|
+
data_api: str
|
|
38
|
+
response_api: str
|
|
39
|
+
is_end: bool
|
|
40
|
+
next: str
|
|
41
|
+
input_files: list[str]
|
|
42
|
+
service_name: str
|
|
43
|
+
|
|
44
|
+
def is_start_command(self):
|
|
45
|
+
"""Determine if this command is a start command."""
|
|
46
|
+
return self.cmd == "start"
|
|
47
|
+
|
|
48
|
+
def is_dataset_command(self):
|
|
49
|
+
"""Determine if this command is a dataset command."""
|
|
50
|
+
return self.cmd == "dataset"
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def __safe_str_to_bool(value):
|
|
54
|
+
"""Safely convert a string to a boolean."""
|
|
55
|
+
if isinstance(value, bool):
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
value_lower = value.lower()
|
|
59
|
+
if value_lower in {'true', '1', 'yes'}:
|
|
60
|
+
return True
|
|
61
|
+
if value_lower in {'false', '0', 'no'}:
|
|
62
|
+
return False
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def __extract_data_from_request(request: Request):
|
|
67
|
+
"""
|
|
68
|
+
Extract data from a Flask request object based on its content type.
|
|
69
|
+
"""
|
|
70
|
+
if request.content_type == 'application/json':
|
|
71
|
+
return request.get_json() or {}
|
|
72
|
+
elif request.content_type.startswith('multipart/form-data'):
|
|
73
|
+
return request.form.to_dict()
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def __allowed_file(filename):
|
|
78
|
+
"""Check if the file has an allowed extension."""
|
|
79
|
+
return filename.lower().endswith('.zip')
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def __extract_input_file(zip_file, service_name, token):
|
|
83
|
+
"""
|
|
84
|
+
Extract a zip file into a temporary directory and return the file paths.
|
|
85
|
+
"""
|
|
86
|
+
if not zip_file.filename:
|
|
87
|
+
log("No file selected for upload.")
|
|
88
|
+
return [], "No selected file"
|
|
89
|
+
|
|
90
|
+
if not RequestCommand.__allowed_file(zip_file.filename):
|
|
91
|
+
log("Invalid file type uploaded.")
|
|
92
|
+
return [], "Invalid file type"
|
|
93
|
+
|
|
94
|
+
temp_dir = TempfileUtils.get_temp_file_name(service_name, token, "dataset_")
|
|
95
|
+
zip_file_path = f"{temp_dir}.zip"
|
|
96
|
+
zip_file.save(zip_file_path)
|
|
97
|
+
|
|
98
|
+
extract_dir = Path(temp_dir)
|
|
99
|
+
extract_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
|
103
|
+
zip_ref.extractall(extract_dir)
|
|
104
|
+
|
|
105
|
+
# Remove unnecessary files or directories
|
|
106
|
+
for unwanted in ['__MACOSX', '.DS_Store']:
|
|
107
|
+
unwanted_path = extract_dir / unwanted
|
|
108
|
+
if unwanted_path.exists():
|
|
109
|
+
if unwanted_path.is_dir():
|
|
110
|
+
os.rmdir(unwanted_path)
|
|
111
|
+
else:
|
|
112
|
+
unwanted_path.unlink()
|
|
113
|
+
|
|
114
|
+
input_files = sorted(str(file) for file in extract_dir.iterdir() if file.is_file())
|
|
115
|
+
log(f"Extracted files: {input_files}")
|
|
116
|
+
return input_files, ""
|
|
117
|
+
|
|
118
|
+
except zipfile.BadZipFile:
|
|
119
|
+
log("Failed to extract zip file.")
|
|
120
|
+
return [], "Invalid or corrupted zip file"
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def parse(request, service_name):
|
|
124
|
+
"""
|
|
125
|
+
Create a RequestCommand object from an HTTP request.
|
|
126
|
+
"""
|
|
127
|
+
params = RequestCommand.__extract_data_from_request(request)
|
|
128
|
+
|
|
129
|
+
token = params.pop("token", "")
|
|
130
|
+
cmd = params.pop("cmd", "")
|
|
131
|
+
data_api = params.pop("data", "")
|
|
132
|
+
response_api = params.pop("response", "")
|
|
133
|
+
is_end = RequestCommand.__safe_str_to_bool(params.pop("is_end", "true"))
|
|
134
|
+
next_page = params.pop("next", "")
|
|
135
|
+
|
|
136
|
+
input_files = []
|
|
137
|
+
error_message = ""
|
|
138
|
+
|
|
139
|
+
if not token:
|
|
140
|
+
error_message = "The `token` parameter is required."
|
|
141
|
+
elif cmd == "dataset":
|
|
142
|
+
zip_file = request.files.get('file')
|
|
143
|
+
if not zip_file:
|
|
144
|
+
error_message = "A file must be uploaded for the `dataset` command."
|
|
145
|
+
else:
|
|
146
|
+
input_files, error_message = RequestCommand.__extract_input_file(
|
|
147
|
+
zip_file, service_name, token
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return RequestCommand(
|
|
151
|
+
runtime_parameters=params,
|
|
152
|
+
token=token,
|
|
153
|
+
cmd=cmd,
|
|
154
|
+
data_api=data_api,
|
|
155
|
+
response_api=response_api,
|
|
156
|
+
is_end=is_end,
|
|
157
|
+
next=next_page,
|
|
158
|
+
input_files=input_files,
|
|
159
|
+
service_name=service_name,
|
|
160
|
+
), error_message
|