pam-python 0.1.22__tar.gz → 0.1.24__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.22/pam_python.egg-info → pam_python-0.1.24}/PKG-INFO +1 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/interface_task_manager.py +8 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/service.py +2 -58
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/task_manager.py +73 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/docker/Dockerfile +2 -2
- pam_python-0.1.24/pam/templates/init/run_test.sh +7 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/service/service.test.tmpl +12 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/service/service_class.tmpl +10 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/tester_task.py +34 -1
- {pam_python-0.1.22 → pam_python-0.1.24/pam_python.egg-info}/PKG-INFO +1 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/setup.py +8 -8
- pam_python-0.1.22/pam/templates/init/run_test.sh +0 -1
- {pam_python-0.1.22 → pam_python-0.1.24}/LICENSE.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/README.md +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/__init__.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/api.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/cli.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/models/__init__.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/models/request_command.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/server.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/sqlite.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/temp_file_utils.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/service/functions.tmpl +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/templates/service/service.yaml +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam/utils.py +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam_python.egg-info/SOURCES.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.1.22 → pam_python-0.1.24}/setup.cfg +0 -0
|
@@ -21,6 +21,14 @@ class ITaskManager(ABC):
|
|
|
21
21
|
def service_request_data(self, service: Service, page):
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def service_request_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False) -> str:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def service_upload_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False, sqlite_file: str = "") -> str:
|
|
30
|
+
pass
|
|
31
|
+
|
|
24
32
|
@abstractmethod
|
|
25
33
|
def service_upload_result(self, service: Service, file_path):
|
|
26
34
|
pass
|
|
@@ -109,41 +109,12 @@ class Service:
|
|
|
109
109
|
|
|
110
110
|
return report_csv_path
|
|
111
111
|
|
|
112
|
-
|
|
113
112
|
def _request_sqlite(self, file_name: str = "", is_shared: bool = False) -> str:
|
|
114
113
|
"""Requests last sqlite file that this plugin has uploaded from the last time.
|
|
115
|
-
|
|
116
114
|
Returns:
|
|
117
115
|
The file path to the downloaded SQLite file, or None if the download failed.
|
|
118
116
|
"""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
endpoint = self.request.sqlite_download
|
|
122
|
-
token = self.request.token
|
|
123
|
-
json_data = {"file_name": file_name, "is_shared": is_shared, "token": token}
|
|
124
|
-
|
|
125
|
-
# Create a unique suffix using current datetime
|
|
126
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
127
|
-
unique_file_name = f"{file_name or 'latest'}_{timestamp}"
|
|
128
|
-
|
|
129
|
-
# Create a temp file path for the downloaded .sqlite file
|
|
130
|
-
output_path = TempfileUtils.get_temp_file_name(
|
|
131
|
-
self.request.service_name,
|
|
132
|
-
token,
|
|
133
|
-
f"sqlite_{unique_file_name}_",
|
|
134
|
-
".sqlite"
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Download SQLite file
|
|
138
|
-
success = self.task_manager.api.download_sqlite_from_post(endpoint, json_data, output_path)
|
|
139
|
-
|
|
140
|
-
if success:
|
|
141
|
-
log(f"{self.request.service_name}: Successfully downloaded SQLite file to {output_path}")
|
|
142
|
-
return output_path
|
|
143
|
-
else:
|
|
144
|
-
log(f"{self.request.service_name}: Failed to download SQLite file.")
|
|
145
|
-
return output_path
|
|
146
|
-
|
|
117
|
+
return self.task_manager.service_request_sqlite(self, file_name, is_shared)
|
|
147
118
|
|
|
148
119
|
def _upload_sqlite(self, file_name: str = "", is_shared: bool = False, sqlite_file: str = "") -> str:
|
|
149
120
|
"""
|
|
@@ -154,34 +125,7 @@ class Service:
|
|
|
154
125
|
:param sqlite_file: Path to the SQLite file to upload.
|
|
155
126
|
:return: The uploaded file name if successful, or empty string if failed.
|
|
156
127
|
"""
|
|
157
|
-
|
|
158
|
-
log(f"{self.request.service_name}: No sqlite file provided for upload.")
|
|
159
|
-
return ""
|
|
160
|
-
|
|
161
|
-
if not Path(sqlite_file).is_file():
|
|
162
|
-
log(f"{self.request.service_name}: SQLite file does not exist: {sqlite_file}")
|
|
163
|
-
return ""
|
|
164
|
-
|
|
165
|
-
endpoint = self.request.sqlite_upload
|
|
166
|
-
payload = {
|
|
167
|
-
"file_name": file_name,
|
|
168
|
-
"is_shared": str(is_shared).lower(), # form data values must be string
|
|
169
|
-
"token": self.request.token
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try:
|
|
173
|
-
log(f"{self.request.service_name}: Uploading sqlite file: {sqlite_file} with payload: {payload}")
|
|
174
|
-
response = self.task_manager.api.http_upload(endpoint, sqlite_file, payload)
|
|
175
|
-
|
|
176
|
-
if response and response.ok:
|
|
177
|
-
log(f"{self.request.service_name}: SQLite file uploaded successfully.")
|
|
178
|
-
return file_name or Path(sqlite_file).name
|
|
179
|
-
else:
|
|
180
|
-
log(f"{self.request.service_name}: Failed to upload sqlite. Status: {response.status_code if response else 'N/A'}")
|
|
181
|
-
return ""
|
|
182
|
-
except Exception as e:
|
|
183
|
-
log(f"{self.request.service_name}: Exception while uploading sqlite: {e}")
|
|
184
|
-
return ""
|
|
128
|
+
return self.task_manager.service_upload_sqlite(self, file_name, is_shared, sqlite_file)
|
|
185
129
|
|
|
186
130
|
def _exit(self) -> None:
|
|
187
131
|
"""Signals the task manager to exit the service."""
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime, timedelta
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
import time
|
|
3
4
|
import threading
|
|
4
5
|
from typing import Dict
|
|
@@ -7,7 +8,7 @@ from pam.api import API
|
|
|
7
8
|
from pam.service import Service
|
|
8
9
|
from pam.models.request_command import RequestCommand
|
|
9
10
|
from pam.interface_task_manager import ITaskManager
|
|
10
|
-
|
|
11
|
+
from pam.temp_file_utils import TempfileUtils
|
|
11
12
|
|
|
12
13
|
class ServiceHolder:
|
|
13
14
|
def __init__(self, service: Service):
|
|
@@ -152,6 +153,77 @@ class TaskManager(ITaskManager):
|
|
|
152
153
|
http_thread = threading.Thread(target=api_call_wrapper)
|
|
153
154
|
http_thread.start()
|
|
154
155
|
|
|
156
|
+
def service_request_sqlite(self, service: Service, file_name="", is_shared=False):
|
|
157
|
+
"""Requests last sqlite file that this plugin has uploaded from the last time.
|
|
158
|
+
Returns:
|
|
159
|
+
The file path to the downloaded SQLite file, or None if the download failed.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
log(f"{service.request.service_name}: Requesting sqlite for file_name={file_name}")
|
|
163
|
+
|
|
164
|
+
endpoint = service.request.sqlite_download
|
|
165
|
+
token = service.request.token
|
|
166
|
+
json_data = {"file_name": file_name, "is_shared": is_shared, "token": token}
|
|
167
|
+
|
|
168
|
+
# Create a unique suffix using current datetime
|
|
169
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
170
|
+
unique_file_name = f"{file_name or 'latest'}_{timestamp}"
|
|
171
|
+
|
|
172
|
+
# Create a temp file path for the downloaded .sqlite file
|
|
173
|
+
output_path = TempfileUtils.get_temp_file_name(
|
|
174
|
+
service.request.service_name,
|
|
175
|
+
token,
|
|
176
|
+
f"sqlite_{unique_file_name}_",
|
|
177
|
+
".sqlite"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Download SQLite file
|
|
181
|
+
success = self.api.download_sqlite_from_post(endpoint, json_data, output_path)
|
|
182
|
+
|
|
183
|
+
if success:
|
|
184
|
+
log(f"{service.request.service_name}: Successfully downloaded SQLite file to {output_path}")
|
|
185
|
+
return output_path
|
|
186
|
+
else:
|
|
187
|
+
log(f"{service.request.service_name}: Failed to download SQLite file.")
|
|
188
|
+
return output_path
|
|
189
|
+
|
|
190
|
+
def service_upload_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False, sqlite_file: str = "") -> str:
|
|
191
|
+
"""
|
|
192
|
+
Uploads the result SQLite file to CDP.
|
|
193
|
+
|
|
194
|
+
:param file_name: The logical file name being uploaded.
|
|
195
|
+
:param is_shared: Whether this file should be treated as shared.
|
|
196
|
+
:param sqlite_file: Path to the SQLite file to upload.
|
|
197
|
+
:return: The uploaded file name if successful, or empty string if failed.
|
|
198
|
+
"""
|
|
199
|
+
if not sqlite_file:
|
|
200
|
+
log(f"{service.request.service_name}: No sqlite file provided for upload.")
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
if not Path(sqlite_file).is_file():
|
|
204
|
+
log(f"{service.request.service_name}: SQLite file does not exist: {sqlite_file}")
|
|
205
|
+
return ""
|
|
206
|
+
|
|
207
|
+
endpoint = service.request.sqlite_upload
|
|
208
|
+
payload = {
|
|
209
|
+
"file_name": file_name,
|
|
210
|
+
"is_shared": str(is_shared).lower(), # form data values must be string
|
|
211
|
+
"token": service.request.token
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
log(f"{service.request.service_name}: Uploading sqlite file: {sqlite_file} with payload: {payload}")
|
|
216
|
+
response = self.api.http_upload(endpoint, sqlite_file, payload)
|
|
217
|
+
|
|
218
|
+
if response and response.ok:
|
|
219
|
+
log(f"{service.request.service_name}: SQLite file uploaded successfully.")
|
|
220
|
+
return file_name or Path(sqlite_file).name
|
|
221
|
+
else:
|
|
222
|
+
log(f"{service.request.service_name}: Failed to upload sqlite. Status: {response.status_code if response else 'N/A'}")
|
|
223
|
+
return ""
|
|
224
|
+
except Exception as e:
|
|
225
|
+
log(f"{service.request.service_name}: Exception while uploading sqlite: {e}")
|
|
226
|
+
return ""
|
|
155
227
|
|
|
156
228
|
def service_upload_result(self, service: Service, file_path):
|
|
157
229
|
"""
|
|
@@ -9,7 +9,7 @@ from pam.tester_task import TesterTask
|
|
|
9
9
|
from pam.models.request_command import RequestCommand
|
|
10
10
|
from pam.temp_file_utils import TempfileUtils
|
|
11
11
|
#pylint: disable=import-error
|
|
12
|
-
from
|
|
12
|
+
from #MODULE_NAME#.#CLASS_NAME# import #CLASS_NAME#
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
# ==================== GLOBAL VARIABLES ====================
|
|
@@ -50,6 +50,13 @@ def on_request_data(page) -> Tuple[List[str], bool, str]:
|
|
|
50
50
|
mock_csv = f"{MOCK_DIR}/mock_data_{page}.csv"
|
|
51
51
|
return [mock_csv], is_end, next_page
|
|
52
52
|
|
|
53
|
+
def on_request_sqlite():
|
|
54
|
+
sqlite_path = f"{MOCK_DIR}/sqlite_path.sqlite"
|
|
55
|
+
return sqlite_path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def on_upload_sqlite(sqlite_path):
|
|
59
|
+
print(f"Upload SQLite {sqlite_path}")
|
|
53
60
|
|
|
54
61
|
def on_upload_report(report_file):
|
|
55
62
|
print(f"Uploaded Report {report_file}")
|
|
@@ -77,6 +84,8 @@ class Test#CLASS_NAME#(unittest.TestCase):
|
|
|
77
84
|
self.testerTask.set_on_upload_result(on_upload_result)
|
|
78
85
|
self.testerTask.set_on_request_data(on_request_data)
|
|
79
86
|
self.testerTask.set_on_upload_report(on_upload_report)
|
|
87
|
+
self.testerTask.set_on_upload_sqlite(on_upload_sqlite)
|
|
88
|
+
self.testerTask.set_on_request_sqlite(on_request_sqlite)
|
|
80
89
|
|
|
81
90
|
def test_run_service(self):
|
|
82
91
|
#---- Create Request Command
|
|
@@ -88,6 +97,8 @@ class Test#CLASS_NAME#(unittest.TestCase):
|
|
|
88
97
|
RESPONSE_API="https://localhost.test/api/dataadapter/test"
|
|
89
98
|
|
|
90
99
|
req = RequestCommand(
|
|
100
|
+
sqlite_download="",
|
|
101
|
+
sqlite_upload="",
|
|
91
102
|
runtime_parameters=runtime_parameters,
|
|
92
103
|
token=token,
|
|
93
104
|
cmd="start",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import threading
|
|
1
2
|
from pam.service import Service
|
|
2
3
|
from pam.models.request_command import RequestCommand
|
|
3
4
|
from pam.utils import log
|
|
4
5
|
#pylint: disable=import-error
|
|
5
|
-
from
|
|
6
|
+
from #MODULE_NAME#.functions import create_report, load_csv_file
|
|
6
7
|
|
|
7
8
|
class #CLASS_NAME#(Service):
|
|
8
9
|
|
|
@@ -22,6 +23,14 @@ class #CLASS_NAME#(Service):
|
|
|
22
23
|
def on_data_input(self, req: RequestCommand):
|
|
23
24
|
log(f"on_data_input req.is_end = {req.is_end}")
|
|
24
25
|
|
|
26
|
+
#RUN IN A SEPERATED THREAD TO PREVENT HTTP REQUEST BLOCKING
|
|
27
|
+
thread = threading.Thread(
|
|
28
|
+
target=lambda: self.__run_process_data_in_thread(req),
|
|
29
|
+
daemon=True,
|
|
30
|
+
)
|
|
31
|
+
thread.start()
|
|
32
|
+
|
|
33
|
+
def __run_process_data_in_thread(self, req: RequestCommand):
|
|
25
34
|
dataframe = self.__process_data(req.input_files)
|
|
26
35
|
self._upload_result(dataframe)
|
|
27
36
|
|
|
@@ -5,10 +5,11 @@ from pam.service import Service
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
RequestDataCallbackType = Callable[[str], Tuple[List[str], bool, str]]
|
|
8
|
+
RequestSqliteCallbackType = Callable[[str, bool], Tuple[str]]
|
|
9
|
+
UploadSqliteCallbackType = Callable[[str, bool, str], Tuple[str]]
|
|
8
10
|
UploadResultCallbackType = Callable[[str], None]
|
|
9
11
|
UploadReportCallbackType = Callable[[str], None]
|
|
10
12
|
|
|
11
|
-
|
|
12
13
|
class TesterTask(ITaskManager):
|
|
13
14
|
"""
|
|
14
15
|
Mock implementation of ITaskManager for testing services.
|
|
@@ -18,6 +19,8 @@ class TesterTask(ITaskManager):
|
|
|
18
19
|
|
|
19
20
|
def __init__(self):
|
|
20
21
|
self.request_data_callback: Optional[RequestDataCallbackType] = None
|
|
22
|
+
self.request_sqlite_callback: Optional[RequestSqliteCallbackType] = None
|
|
23
|
+
self.upload_sqlite_callback: Optional[UploadSqliteCallbackType] = None
|
|
21
24
|
self.upload_result_callback: Optional[UploadResultCallbackType] = None
|
|
22
25
|
self.upload_report_callback: Optional[UploadReportCallbackType] = None
|
|
23
26
|
|
|
@@ -27,6 +30,18 @@ class TesterTask(ITaskManager):
|
|
|
27
30
|
"""
|
|
28
31
|
self.request_data_callback = request_data_callback
|
|
29
32
|
|
|
33
|
+
def set_on_request_sqlite(self, request_sqlite_callback: RequestSqliteCallbackType):
|
|
34
|
+
"""
|
|
35
|
+
Set the callback for simulating data requests.
|
|
36
|
+
"""
|
|
37
|
+
self.request_sqlite_callback = request_sqlite_callback
|
|
38
|
+
|
|
39
|
+
def set_on_upload_sqlite(self, upload_sqlite_callback: UploadSqliteCallbackType):
|
|
40
|
+
"""
|
|
41
|
+
Set the callback for simulating SQLite uploads.
|
|
42
|
+
"""
|
|
43
|
+
self.upload_sqlite_callback = upload_sqlite_callback
|
|
44
|
+
|
|
30
45
|
def set_on_upload_result(self, upload_result_callback: UploadResultCallbackType):
|
|
31
46
|
"""
|
|
32
47
|
Set the callback for simulating result uploads.
|
|
@@ -89,6 +104,24 @@ class TesterTask(ITaskManager):
|
|
|
89
104
|
else:
|
|
90
105
|
print("Request data callback is not set.")
|
|
91
106
|
|
|
107
|
+
def service_request_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False) -> str:
|
|
108
|
+
if self.request_sqlite_callback is not None:
|
|
109
|
+
try:
|
|
110
|
+
return self.request_sqlite_callback(file_name, is_shared)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error in service_request_sqlite: {e}")
|
|
113
|
+
else:
|
|
114
|
+
print("Request sqlite callback is not set.")
|
|
115
|
+
|
|
116
|
+
def service_upload_sqlite(self, service: Service, file_name: str = "", is_shared: bool = False, sqlite_file: str = "") -> str:
|
|
117
|
+
if self.upload_sqlite_callback is not None:
|
|
118
|
+
try:
|
|
119
|
+
return self.upload_sqlite_callback(file_name, is_shared, sqlite_file)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"Error in service_upload_sqlite: {e}")
|
|
122
|
+
else:
|
|
123
|
+
print("Upload sqlite callback is not set.")
|
|
124
|
+
|
|
92
125
|
def service_upload_result(self, service: Service, file_path: str):
|
|
93
126
|
"""
|
|
94
127
|
Simulate a service result upload.
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
from setuptools import setup, find_packages
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
|
-
name="pam-python",
|
|
5
|
-
version="0.1.
|
|
4
|
+
name="pam-python",
|
|
5
|
+
version="0.1.24",
|
|
6
6
|
author="Narongrit Kanhanoi",
|
|
7
7
|
author_email="narongrit@pams.ai",
|
|
8
8
|
description="Pam Python Library",
|
|
9
9
|
long_description=open("README.md", encoding='utf-8').read(),
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
11
|
-
url="https://github.com/heart/pam-python",
|
|
12
|
-
packages=find_packages(),
|
|
11
|
+
url="https://github.com/heart/pam-python",
|
|
12
|
+
packages=find_packages(),
|
|
13
13
|
classifiers=[
|
|
14
14
|
"Programming Language :: Python :: 3",
|
|
15
15
|
"Programming Language :: Python :: 3.8",
|
|
16
16
|
"Programming Language :: Python :: 3.9",
|
|
17
|
-
"License :: Other/Proprietary License",
|
|
17
|
+
"License :: Other/Proprietary License",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Intended Audience :: Developers",
|
|
20
20
|
"Intended Audience :: Customer Service",
|
|
21
21
|
"Topic :: Software Development :: Libraries",
|
|
22
22
|
],
|
|
23
23
|
package_data={
|
|
24
|
-
"pam": ["templates/**/*"],
|
|
24
|
+
"pam": ["templates/**/*"],
|
|
25
25
|
},
|
|
26
26
|
install_requires=[
|
|
27
27
|
"setuptools>=70.0.0",
|
|
@@ -37,8 +37,8 @@ setup(
|
|
|
37
37
|
],
|
|
38
38
|
entry_points={
|
|
39
39
|
"console_scripts": [
|
|
40
|
-
"pam=pam.cli:main",
|
|
40
|
+
"pam=pam.cli:main",
|
|
41
41
|
],
|
|
42
42
|
},
|
|
43
|
-
python_requires=">=3.8",
|
|
43
|
+
python_requires=">=3.8",
|
|
44
44
|
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
python -m unittest discover
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|