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.
Files changed (44) hide show
  1. pam_python-0.1.2/PKG-INFO +126 -0
  2. pam_python-0.1.2/README.md +99 -0
  3. pam_python-0.1.2/pam/api.py +53 -0
  4. {pam_python-0.1.0 → pam_python-0.1.2}/pam/cli.py +37 -10
  5. pam_python-0.1.2/pam/models/request_command.py +160 -0
  6. pam_python-0.1.2/pam/server.py +180 -0
  7. pam_python-0.1.2/pam/service.py +124 -0
  8. {pam_python-0.1.0 → pam_python-0.1.2}/pam/task_manager.py +56 -34
  9. pam_python-0.1.2/pam/temp_file_utils.py +100 -0
  10. pam_python-0.1.2/pam/templates/init/dockerignore.tmpl +4 -0
  11. pam_python-0.1.2/pam/templates/init/gitignore.tmpl +178 -0
  12. pam_python-0.1.2/pam/templates/init/pylintrc.tmpl +2 -0
  13. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/service.test.tmpl +35 -16
  14. pam_python-0.1.0/pam/templates/service/ServiceClass.tmpl → pam_python-0.1.2/pam/templates/service/service_class.tmpl +9 -7
  15. pam_python-0.1.2/pam/tester_task.py +118 -0
  16. pam_python-0.1.2/pam/utils.py +60 -0
  17. pam_python-0.1.2/pam_python.egg-info/PKG-INFO +126 -0
  18. {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/SOURCES.txt +4 -1
  19. {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/requires.txt +1 -0
  20. {pam_python-0.1.0 → pam_python-0.1.2}/setup.py +4 -3
  21. pam_python-0.1.0/PKG-INFO +0 -29
  22. pam_python-0.1.0/README.md +0 -3
  23. pam_python-0.1.0/pam/api.py +0 -42
  24. pam_python-0.1.0/pam/models/request_command.py +0 -138
  25. pam_python-0.1.0/pam/server.py +0 -170
  26. pam_python-0.1.0/pam/service.py +0 -101
  27. pam_python-0.1.0/pam/temp_file_utils.py +0 -75
  28. pam_python-0.1.0/pam/tester_task.py +0 -60
  29. pam_python-0.1.0/pam/utils.py +0 -38
  30. pam_python-0.1.0/pam_python.egg-info/PKG-INFO +0 -29
  31. {pam_python-0.1.0 → pam_python-0.1.2}/LICENSE.txt +0 -0
  32. {pam_python-0.1.0 → pam_python-0.1.2}/pam/__init__.py +0 -0
  33. {pam_python-0.1.0 → pam_python-0.1.2}/pam/interface_task_manager.py +0 -0
  34. {pam_python-0.1.0 → pam_python-0.1.2}/pam/models/__init__.py +0 -0
  35. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/buildcmd/pamb +0 -0
  36. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/buildcmd/pamb-base.sh +0 -0
  37. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/docker/Dockerfile +0 -0
  38. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/init/main.tmpl +0 -0
  39. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/functions.tmpl +0 -0
  40. {pam_python-0.1.0 → pam_python-0.1.2}/pam/templates/service/service.yaml +0 -0
  41. {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/dependency_links.txt +0 -0
  42. {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/entry_points.txt +0 -0
  43. {pam_python-0.1.0 → pam_python-0.1.2}/pam_python.egg-info/top_level.txt +0 -0
  44. {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 pkg_resources
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
- type = args[1]
20
- if type == "service":
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/ServiceClass.tmpl", os.path.join(name, name+"_service.py") )
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