pam-python 0.1.29__tar.gz → 0.1.31__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.29 → pam_python-0.1.31}/PKG-INFO +1 -1
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/cli.py +2 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/interface_task_manager.py +2 -2
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/service.py +9 -5
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/task_manager.py +14 -3
- pam_python-0.1.31/pam/templates/init/AGENT.md +191 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/service/service.test.tmpl +1 -1
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/tester_task.py +17 -4
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/PKG-INFO +1 -1
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/SOURCES.txt +1 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/setup.py +1 -1
- {pam_python-0.1.29 → pam_python-0.1.31}/LICENSE.txt +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/README.md +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/__init__.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/api.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/models/__init__.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/models/request_command.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/server.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/sqlite.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/temp_file_utils.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/buildcmd/pamb +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/buildcmd/pamb-base.sh +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/docker/Dockerfile +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/dockerignore.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/gitignore.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/main.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/pylintrc.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/run_unit_test.bat +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/run_unit_test.ps1 +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/init/run_unit_test.sh +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/service/functions.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/service/service.yaml +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/templates/service/service_class.tmpl +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam/utils.py +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/dependency_links.txt +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/entry_points.txt +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/requires.txt +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/pam_python.egg-info/top_level.txt +0 -0
- {pam_python-0.1.29 → pam_python-0.1.31}/setup.cfg +0 -0
|
@@ -74,6 +74,7 @@ def create_service(name):
|
|
|
74
74
|
cpy("service/service.yaml", os.path.join(name, "service.yaml"))
|
|
75
75
|
cpy("service/functions.tmpl", os.path.join(name, "functions.py"))
|
|
76
76
|
cpy("service/service.test.tmpl", os.path.join(name, f"test_{name}.py"))
|
|
77
|
+
|
|
77
78
|
|
|
78
79
|
replace_template_content(name, to_pascal_case(
|
|
79
80
|
name)+"Svc", to_pascal_case(name)+"Svc.py")
|
|
@@ -96,6 +97,7 @@ def init_project():
|
|
|
96
97
|
cpy("init/run_unit_test.sh", "run_unit_test.sh")
|
|
97
98
|
cpy("init/run_unit_test.bat", "run_unit_test.bat")
|
|
98
99
|
cpy("init/run_unit_test.ps1", "run_unit_test.ps1")
|
|
100
|
+
cpy("init/AGENT.md", "AGENT.md")
|
|
99
101
|
|
|
100
102
|
if not os.path.exists("requirements.txt"):
|
|
101
103
|
open("requirements.txt", 'a', encoding='utf-8').close()
|
|
@@ -18,7 +18,7 @@ class ITaskManager(ABC):
|
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
20
|
@abstractmethod
|
|
21
|
-
def service_request_data(self, service: Service, page):
|
|
21
|
+
def service_request_data(self, service: Service, page, filter_contact_ids=None):
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
@abstractmethod
|
|
@@ -30,7 +30,7 @@ class ITaskManager(ABC):
|
|
|
30
30
|
pass
|
|
31
31
|
|
|
32
32
|
@abstractmethod
|
|
33
|
-
def service_upload_result(self, service: Service, file_path):
|
|
33
|
+
def service_upload_result(self, service: Service, file_path, options=None):
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
36
|
@abstractmethod
|
|
@@ -2,7 +2,7 @@ from abc import abstractmethod
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
import json
|
|
5
|
-
from typing import TYPE_CHECKING, Union
|
|
5
|
+
from typing import TYPE_CHECKING, Union, Optional, List
|
|
6
6
|
import pandas as pd
|
|
7
7
|
import dask.dataframe as dd
|
|
8
8
|
from pam.utils import log, deep_convert_numbers_to_strings, get_adapter_id
|
|
@@ -29,13 +29,17 @@ class Service:
|
|
|
29
29
|
return get_adapter_id(self.request.response_api)
|
|
30
30
|
|
|
31
31
|
# == REQUEST DATA ===
|
|
32
|
-
def _request_data(self, page: str = "") -> None:
|
|
32
|
+
def _request_data(self, page: str = "", filter_contact_ids: Optional[List[str]] = None) -> None:
|
|
33
33
|
"""Requests data for the specified page."""
|
|
34
34
|
log(f"{self.request.service_name}: Requesting data for page={page}")
|
|
35
|
-
self.task_manager.service_request_data(self, page)
|
|
35
|
+
self.task_manager.service_request_data(self, page, filter_contact_ids)
|
|
36
36
|
|
|
37
37
|
# == UPLOAD RESULT ===
|
|
38
|
-
def _upload_result(
|
|
38
|
+
def _upload_result(
|
|
39
|
+
self,
|
|
40
|
+
df: Union[pd.DataFrame, dd.DataFrame],
|
|
41
|
+
options: Optional[dict] = None,
|
|
42
|
+
) -> str:
|
|
39
43
|
"""
|
|
40
44
|
Uploads the result DataFrame to a temporary file and notifies the task manager.
|
|
41
45
|
"""
|
|
@@ -56,7 +60,7 @@ class Service:
|
|
|
56
60
|
try:
|
|
57
61
|
df.to_csv(tmp_file_name, index=False)
|
|
58
62
|
log(f"{self.request.service_name}: Uploading result file: {tmp_file_name}")
|
|
59
|
-
self.task_manager.service_upload_result(self, tmp_file_name)
|
|
63
|
+
self.task_manager.service_upload_result(self, tmp_file_name, options)
|
|
60
64
|
except Exception as e:
|
|
61
65
|
log(f"Failed to upload result file: {e}")
|
|
62
66
|
raise
|
|
@@ -127,7 +127,7 @@ class TaskManager(ITaskManager):
|
|
|
127
127
|
self._remove_service(token)
|
|
128
128
|
|
|
129
129
|
# ==== Service Callbacks ====
|
|
130
|
-
def service_request_data(self, service: Service, page):
|
|
130
|
+
def service_request_data(self, service: Service, page, filter_contact_ids=None):
|
|
131
131
|
"""
|
|
132
132
|
Makes an asynchronous request for data from a service.
|
|
133
133
|
Logs the response without blocking the main thread.
|
|
@@ -135,6 +135,8 @@ class TaskManager(ITaskManager):
|
|
|
135
135
|
endpoint = service.request.data_api
|
|
136
136
|
token = service.request.token
|
|
137
137
|
json_data = {"page": page, "token": token}
|
|
138
|
+
if filter_contact_ids is not None:
|
|
139
|
+
json_data["filter_contact_ids"] = filter_contact_ids
|
|
138
140
|
|
|
139
141
|
def handle_response(response):
|
|
140
142
|
"""Logs the response from the API."""
|
|
@@ -225,11 +227,20 @@ class TaskManager(ITaskManager):
|
|
|
225
227
|
log(f"{service.request.service_name}: Exception while uploading sqlite: {e}")
|
|
226
228
|
return ""
|
|
227
229
|
|
|
228
|
-
def service_upload_result(self, service: Service, file_path):
|
|
230
|
+
def service_upload_result(self, service: Service, file_path, options=None):
|
|
229
231
|
"""
|
|
230
232
|
Uploads a result file asynchronously and logs the response.
|
|
231
233
|
"""
|
|
232
234
|
endpoint = service.request.response_api
|
|
235
|
+
payload = None
|
|
236
|
+
if options is not None:
|
|
237
|
+
payload = {}
|
|
238
|
+
if "is_realtime" in options:
|
|
239
|
+
is_realtime = options.get("is_realtime")
|
|
240
|
+
payload["is_realtime"] = str(is_realtime).lower() if isinstance(is_realtime, bool) else is_realtime
|
|
241
|
+
if "is_priority" in options:
|
|
242
|
+
is_priority = options.get("is_priority")
|
|
243
|
+
payload["is_priority"] = str(is_priority).lower() if isinstance(is_priority, bool) else is_priority
|
|
233
244
|
|
|
234
245
|
def handle_upload_response(response):
|
|
235
246
|
"""Logs the response after the upload completes."""
|
|
@@ -241,7 +252,7 @@ class TaskManager(ITaskManager):
|
|
|
241
252
|
|
|
242
253
|
def upload_wrapper():
|
|
243
254
|
"""Wrapper for the upload to handle response logging."""
|
|
244
|
-
response = self.api.http_upload(endpoint, file_path)
|
|
255
|
+
response = self.api.http_upload(endpoint, file_path, payload)
|
|
245
256
|
handle_upload_response(response)
|
|
246
257
|
|
|
247
258
|
log(f"Uploading Result to: {endpoint}")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# AGENT
|
|
2
|
+
|
|
3
|
+
This document explains the data plugin service lifecycle so developers can create new services and understand runtime behavior inside the CDP environment.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
1. This project is a data plugin that will be connected to the main system in production.
|
|
8
|
+
2. The data plugin processes data from CDP and is created as separate services by domain. For example, if we process RFM, we create a new service dedicated to that domain.
|
|
9
|
+
|
|
10
|
+
## Create a New Service
|
|
11
|
+
|
|
12
|
+
1. Create a service with:
|
|
13
|
+
`pam new service [service name]`
|
|
14
|
+
2. The system creates a new folder named `[service name]` with sample code and basic templates.
|
|
15
|
+
3. Example:
|
|
16
|
+
`pam new service rfm_segment`
|
|
17
|
+
This generates `rfm_segment/Rfm_segmentSvc.py`, which is the starting point of the service.
|
|
18
|
+
4. `rfm_segment/service.yaml` registers the service automatically.
|
|
19
|
+
The value `endpoint: /Rfm_segmentSvc` means CDP will call:
|
|
20
|
+
`http://ip-of-this-k8s-pod/services/Rfm_segmentSvc`
|
|
21
|
+
|
|
22
|
+
## Service Lifecycle
|
|
23
|
+
|
|
24
|
+
CDP spawns a service instance when a job starts and terminates it after the job completes. This design supports parallel execution. For example, two runs will spawn two separate instances. The only risk is shared local files (e.g., sqlite) which can cause conflicts.
|
|
25
|
+
|
|
26
|
+
The lifecycle is wired in `rfm_segment/Rfm_segmentSvc.py` (example service name).
|
|
27
|
+
|
|
28
|
+
### on_start
|
|
29
|
+
|
|
30
|
+
`on_start` runs at startup. Use it to read runtime parameters from CDP and initialize work.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
def on_start(self):
|
|
36
|
+
log("on_start")
|
|
37
|
+
|
|
38
|
+
some_param = self.request.runtime_parameters.get("some_param", "")
|
|
39
|
+
self._request_data()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`runtime_parameters` are configured by CDP to control service behavior without code changes (e.g., `period_day="7"`).
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
|
|
46
|
+
- All values are strings. Cast with defaults to avoid errors.
|
|
47
|
+
- `dry_run` is the only built-in parameter. If missing, it runs for real. If `dry_run="true"`, the service runs fully but does not upload results to CDP.
|
|
48
|
+
- You do not need to check `dry_run` manually. Always call `_upload_result`; the system handles it.
|
|
49
|
+
|
|
50
|
+
**Important:** `on_start` must return quickly because CDP calls via HTTP. If it hangs, the client may time out and the job will be terminated. If work is long-running, move it to a thread and return from `on_start` first. This applies to all lifecycle functions.
|
|
51
|
+
|
|
52
|
+
### Request Data from CDP
|
|
53
|
+
|
|
54
|
+
Call `self._request_data()` to ask CDP for input data. CDP returns up to 5 CSV files (each file is a separate event).
|
|
55
|
+
|
|
56
|
+
- File order is guaranteed per agreement, so you can index by position.
|
|
57
|
+
- Column names and record counts are defined by CDP configuration.
|
|
58
|
+
|
|
59
|
+
CSV example:
|
|
60
|
+
|
|
61
|
+
```csv
|
|
62
|
+
_id,id,data1,data2,data3
|
|
63
|
+
xx,yy,zz,nn,mm
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Column meaning
|
|
67
|
+
|
|
68
|
+
- `_id` = event id for dedup/processing checks. Often unused and can be dropped.
|
|
69
|
+
- `id` = customer id used by CDP as a key.
|
|
70
|
+
- Other columns depend on the agreement.
|
|
71
|
+
|
|
72
|
+
Date format is always:
|
|
73
|
+
`"02/08/2026, 18:23:54"`
|
|
74
|
+
Timezone may vary by customer data (usually Bangkok time).
|
|
75
|
+
You may sometimes be asked for the exact time zone if it is logically important for the program.
|
|
76
|
+
|
|
77
|
+
### on_data_input
|
|
78
|
+
|
|
79
|
+
When CDP finishes collecting data, it calls `on_data_input`.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
def on_data_input(self, req: RequestCommand):
|
|
85
|
+
log(f"on_data_input req.is_end = {req.is_end}")
|
|
86
|
+
|
|
87
|
+
# RUN IN A SEPARATED THREAD TO PREVENT HTTP REQUEST BLOCKING
|
|
88
|
+
thread = threading.Thread(
|
|
89
|
+
target=lambda: self.__run_process_data_in_thread(req),
|
|
90
|
+
daemon=True,
|
|
91
|
+
)
|
|
92
|
+
thread.start()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def __run_process_data_in_thread(self, req: RequestCommand):
|
|
96
|
+
dataframe = self.__process_data(req.input_files)
|
|
97
|
+
self._upload_result(dataframe)
|
|
98
|
+
|
|
99
|
+
if not req.is_end:
|
|
100
|
+
self._request_data(req.next)
|
|
101
|
+
else:
|
|
102
|
+
self._exit()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Why a thread: to end the HTTP request quickly and avoid timeouts.
|
|
106
|
+
|
|
107
|
+
`req.input_files` is the ordered list of CSV paths.
|
|
108
|
+
|
|
109
|
+
### Upload Results to CDP
|
|
110
|
+
|
|
111
|
+
After processing, call `_upload_result` with a dataframe. Example output:
|
|
112
|
+
|
|
113
|
+
```csv
|
|
114
|
+
id,data_x,rfm
|
|
115
|
+
xxx,20,hero
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `id` is required for CDP matching.
|
|
119
|
+
- Other columns are defined by agreement.
|
|
120
|
+
|
|
121
|
+
Sometimes you may include an `event` column to create per-user events:
|
|
122
|
+
|
|
123
|
+
```csv
|
|
124
|
+
id,data_x,event
|
|
125
|
+
xxx,20,purchase
|
|
126
|
+
yyy,50,refund
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If `event` is missing, CDP uses the default event configured in the system.
|
|
130
|
+
|
|
131
|
+
### Pagination
|
|
132
|
+
|
|
133
|
+
In the example:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
if not req.is_end:
|
|
137
|
+
self._request_data(req.next)
|
|
138
|
+
else:
|
|
139
|
+
self._exit()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- `req.is_end` indicates whether this is the last page.
|
|
143
|
+
- If not end, call `self._request_data(req.next)` for the next page.
|
|
144
|
+
- If end, call `self._exit()` to terminate the instance.
|
|
145
|
+
|
|
146
|
+
CDP will not send the next page until you call `_request_data`, so the flow is serialized and no two threads will handle the same data unless you explicitly design it that way.
|
|
147
|
+
|
|
148
|
+
### on_terminate
|
|
149
|
+
|
|
150
|
+
Before termination, the system calls:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
def on_terminate(self):
|
|
154
|
+
log("on_terminate")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Use it for cleanup.
|
|
158
|
+
|
|
159
|
+
## Persist State Between Runs (sqlite/duckdb)
|
|
160
|
+
|
|
161
|
+
Services can be killed and restarted at any time. If you need state across runs, use sqlite or duckdb via the provided API.
|
|
162
|
+
|
|
163
|
+
Example (sqlite):
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
SQLITE_FILE_NAME = "mydata.sqlite"
|
|
167
|
+
|
|
168
|
+
sqlite_file = self._request_sqlite(file_name=SQLITE_FILE_NAME, is_shared=False)
|
|
169
|
+
if sqlite_file is not None:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
self._upload_sqlite(
|
|
173
|
+
file_name=SQLITE_FILE_NAME,
|
|
174
|
+
is_shared=False,
|
|
175
|
+
sqlite_file="/local/path/to/upload/sqlite.sqlite",
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Notes
|
|
180
|
+
|
|
181
|
+
- `_request_sqlite` downloads from the server and is automatically scoped to this plugin. It will not mix with other plugins even if the name matches.
|
|
182
|
+
- The call is synchronous and returns the local path.
|
|
183
|
+
- If the return value is `None`, no file exists on the server; you can create a new one.
|
|
184
|
+
- `_upload_sqlite` always overwrites the previous file.
|
|
185
|
+
|
|
186
|
+
`is_shared` behavior
|
|
187
|
+
|
|
188
|
+
- `False` (recommended): isolate per service. Multiple instances get their own copies.
|
|
189
|
+
- `True`: shared storage. If another plugin updates the file, this plugin can download the latest version.
|
|
190
|
+
|
|
191
|
+
Note: `file_name` acts like a server key, while `sqlite_file` is the local file path.
|
|
@@ -35,7 +35,7 @@ def create_mock_csv(page, mockDir):
|
|
|
35
35
|
df.to_csv(file_name, index=False)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
def on_request_data(page) -> Tuple[List[str], bool, str]:
|
|
38
|
+
def on_request_data(page, filter_contact_ids=None) -> Tuple[List[str], bool, str]:
|
|
39
39
|
if page is None or page == "" :
|
|
40
40
|
page = "1"
|
|
41
41
|
|
|
@@ -4,7 +4,7 @@ from pam.interface_task_manager import ITaskManager
|
|
|
4
4
|
from pam.service import Service
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
RequestDataCallbackType = Callable[
|
|
7
|
+
RequestDataCallbackType = Callable[..., Tuple[List[str], bool, str]]
|
|
8
8
|
RequestSqliteCallbackType = Callable[[str, bool], Tuple[str]]
|
|
9
9
|
UploadSqliteCallbackType = Callable[[str, bool, str], Tuple[str]]
|
|
10
10
|
UploadResultCallbackType = Callable[[str], None]
|
|
@@ -81,16 +81,23 @@ class TesterTask(ITaskManager):
|
|
|
81
81
|
while not self.is_exit:
|
|
82
82
|
pass
|
|
83
83
|
|
|
84
|
-
def service_request_data(self, service: Service, page: str):
|
|
84
|
+
def service_request_data(self, service: Service, page: str, filter_contact_ids: Optional[List[str]] = None):
|
|
85
85
|
"""
|
|
86
86
|
Simulate a service data request.
|
|
87
87
|
|
|
88
88
|
:param service: The service requesting data.
|
|
89
89
|
:param page: The page identifier for the data request.
|
|
90
|
+
:param filter_contact_ids: Optional list of contact IDs to filter the request.
|
|
90
91
|
"""
|
|
91
92
|
if self.request_data_callback is not None:
|
|
92
93
|
try:
|
|
93
|
-
|
|
94
|
+
if filter_contact_ids is None:
|
|
95
|
+
files, is_end, next_page = self.request_data_callback(page)
|
|
96
|
+
else:
|
|
97
|
+
try:
|
|
98
|
+
files, is_end, next_page = self.request_data_callback(page, filter_contact_ids)
|
|
99
|
+
except TypeError:
|
|
100
|
+
files, is_end, next_page = self.request_data_callback(page)
|
|
94
101
|
req = RequestCommand(
|
|
95
102
|
sqlite_download="",
|
|
96
103
|
sqlite_upload="",
|
|
@@ -128,12 +135,18 @@ class TesterTask(ITaskManager):
|
|
|
128
135
|
else:
|
|
129
136
|
print("Upload sqlite callback is not set.")
|
|
130
137
|
|
|
131
|
-
def service_upload_result(
|
|
138
|
+
def service_upload_result(
|
|
139
|
+
self,
|
|
140
|
+
service: Service,
|
|
141
|
+
file_path: str,
|
|
142
|
+
options: Optional[dict] = None,
|
|
143
|
+
):
|
|
132
144
|
"""
|
|
133
145
|
Simulate a service result upload.
|
|
134
146
|
|
|
135
147
|
:param service: The service uploading the result.
|
|
136
148
|
:param file_path: The path of the file being uploaded.
|
|
149
|
+
:param options: Optional upload options (e.g. is_realtime, is_priority).
|
|
137
150
|
"""
|
|
138
151
|
if self.upload_result_callback is not None:
|
|
139
152
|
try:
|
|
@@ -17,6 +17,7 @@ pam/models/request_command.py
|
|
|
17
17
|
pam/templates/buildcmd/pamb
|
|
18
18
|
pam/templates/buildcmd/pamb-base.sh
|
|
19
19
|
pam/templates/docker/Dockerfile
|
|
20
|
+
pam/templates/init/AGENT.md
|
|
20
21
|
pam/templates/init/dockerignore.tmpl
|
|
21
22
|
pam/templates/init/gitignore.tmpl
|
|
22
23
|
pam/templates/init/main.tmpl
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|