revnext 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

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.
@@ -1,254 +1,254 @@
1
- """
2
- Download Parts Price List CSV report from Revolution Next (*.revolutionnext.com.au).
3
- Uses cookies (Chrome export format) and configurable base URL.
4
- """
5
-
6
- from datetime import datetime
7
- from pathlib import Path
8
- from typing import Optional
9
-
10
- import requests
11
-
12
- from revnext.common import create_session, run_report_flow
13
- from revnext.config import get_revnext_base_url_from_env
14
-
15
- SERVICE_OBJECT = "Revolution.Activity.IM.RPT.PartsPriceListPR"
16
- ACTIVITY_TAB_ID = "N78b54de4_7cdc_43e0_9e42_71a49bec44f2"
17
-
18
-
19
- def _build_submit_body() -> dict:
20
- """Build the submitActivityTask request body. Use today's date for the trigger."""
21
- tz = "+10:00"
22
- now = datetime.now()
23
- start_date = now.strftime("%Y-%m-%d")
24
- start_time = f"{start_date}T{now.strftime('%H:%M')}:00.000{tz}"
25
- return {
26
- "_userContext_vg_coid": "03",
27
- "_userContext_vg_divid": "1",
28
- "_userContext_vg_dftdpt": "570",
29
- "activityTabId": ACTIVITY_TAB_ID,
30
- "dataSets": [
31
- {
32
- "name": "dsActivityTask",
33
- "id": None,
34
- "dataSet": {
35
- "dsActivityTask": {
36
- "prods:hasChanges": True,
37
- "ttActivityTask": [
38
- {
39
- "prods:id": "ttActivityTask1945856",
40
- "prods:rowState": "created",
41
- "fldId": 1,
42
- "taskID": "",
43
- "loadRowidPassThrough": "dummy",
44
- "executeActivityTaskNow": False,
45
- "executedAt": None,
46
- "logMessages": "",
47
- "startTime": None,
48
- "endTime": None,
49
- }
50
- ],
51
- "prods:before": {},
52
- }
53
- },
54
- },
55
- {
56
- "name": "dsActivityTaskTriggers",
57
- "id": None,
58
- "dataSet": {
59
- "dsActivityTaskTriggers": {
60
- "prods:hasChanges": True,
61
- "ttActivityTaskTrigger": [
62
- {
63
- "prods:id": "ttActivityTaskTrigger1786112",
64
- "prods:rowState": "created",
65
- "fldId": 1,
66
- "mode": "O",
67
- "startDateTime": start_time,
68
- "startDate": f"{start_date}T00:00:00.000{tz}",
69
- "startHour": now.hour,
70
- "startMinute": now.minute,
71
- "recurEvery": 1,
72
- "recurEveryUOM": "days",
73
- "weeklySun": False,
74
- "weeklyMon": False,
75
- "weeklyTue": False,
76
- "weeklyWed": False,
77
- "weeklyThu": False,
78
- "weeklyFri": False,
79
- "weeklySat": False,
80
- "monthsList": "",
81
- "monthlyMode": "",
82
- "monthlyDaysList": "",
83
- "monthlyOnWeekNumber": "",
84
- "monthlyOnDayOfWeek": "",
85
- "triggerDescription": "",
86
- "triggerNextSchedule": None,
87
- "windowTimeFrom": "",
88
- "windowTimeTo": "",
89
- "windowAllDay": False,
90
- }
91
- ],
92
- "prods:before": {},
93
- }
94
- },
95
- },
96
- {
97
- "name": "dsParams",
98
- "id": None,
99
- "dataSet": {
100
- "dsParams": {
101
- "prods:hasChanges": True,
102
- "tt_params": [
103
- {
104
- "prods:id": "tt_paramsFldId1",
105
- "prods:rowState": "modified",
106
- "fldid": 1,
107
- "coid": "03",
108
- "divid": "1",
109
- "activityid": "IM.RPT.PartsPriceListPR",
110
- "taskid": "",
111
- "tasksts": None,
112
- "rptid": "",
113
- "pdf": "",
114
- "formprt": False,
115
- "csvout": True,
116
- "emailopt": "n",
117
- "emailme": False,
118
- "useremail": "lwarneke@mikecarneytoyota.com.au",
119
- "emailprinter": False,
120
- "prtid": "",
121
- "ddpflg": False,
122
- "ddpquo": 0,
123
- "submitopt": "p",
124
- "email_staff": False,
125
- "staff_email": "",
126
- "email_other": False,
127
- "other_email": "",
128
- "subject": "Parts Price List",
129
- "attn": "",
130
- "email_text": "",
131
- "email_signature": "",
132
- "email_sig_type": "D",
133
- "prttyp": "s",
134
- "dptid": "570",
135
- "frnid": "",
136
- "frnidto": "",
137
- "binid": "",
138
- "binidto": "",
139
- "prctyp1": "L",
140
- "prctyp2": "S",
141
- "incgst1": True,
142
- "incgst2": True,
143
- "exportexcel": False,
144
- "tasktype": "",
145
- }
146
- ],
147
- "prods:before": {
148
- "tt_params": [
149
- {
150
- "prods:id": "tt_paramsFldId1",
151
- "prods:rowState": "modified",
152
- "fldid": 1,
153
- "coid": "03",
154
- "divid": "1",
155
- "activityid": "",
156
- "taskid": "",
157
- "tasksts": None,
158
- "rptid": "",
159
- "pdf": "",
160
- "formprt": False,
161
- "csvout": False,
162
- "emailopt": "",
163
- "emailme": False,
164
- "useremail": "",
165
- "emailprinter": False,
166
- "prtid": "",
167
- "ddpflg": False,
168
- "ddpquo": 0,
169
- "submitopt": "",
170
- "email_staff": False,
171
- "staff_email": "",
172
- "email_other": False,
173
- "other_email": "",
174
- "subject": "",
175
- "attn": "",
176
- "email_text": "",
177
- "email_signature": "",
178
- "email_sig_type": "",
179
- "prttyp": "s",
180
- "dptid": "130",
181
- "frnid": "",
182
- "frnidto": "",
183
- "binid": "",
184
- "binidto": "",
185
- "prctyp1": "",
186
- "prctyp2": "",
187
- "incgst1": False,
188
- "incgst2": False,
189
- "exportexcel": False,
190
- "tasktype": "",
191
- }
192
- ]
193
- },
194
- }
195
- },
196
- },
197
- ],
198
- "stopOnWarning": True,
199
- "validateOnly": False,
200
- "uiType": "ISC",
201
- }
202
-
203
-
204
- def _post_submit_closesubmit_factory(base_url: str):
205
- """Return a hook that calls onChoose_btn_closesubmit (required for Parts Price List flow)."""
206
- def _post_submit_closesubmit(session: requests.Session) -> None:
207
- url = f"{base_url}/next/rest/si/presenter/onChoose_btn_closesubmit"
208
- body = {
209
- "_userContext_vg_coid": "03",
210
- "_userContext_vg_divid": "1",
211
- "_userContext_vg_dftdpt": "570",
212
- "activityTabId": ACTIVITY_TAB_ID,
213
- "ctrlProp": [
214
- {"name": "tt_params.submitopt", "prop": "SCREENVALUE", "value": "p"}
215
- ],
216
- "uiType": "ISC",
217
- }
218
- r = session.post(url, json=body)
219
- r.raise_for_status()
220
- return _post_submit_closesubmit
221
-
222
-
223
- def download_parts_price_list_report(
224
- cookies_path: Optional[Path | str] = None,
225
- output_path: Optional[Path | str] = None,
226
- base_url: Optional[str] = None,
227
- ) -> Path:
228
- """
229
- Run the Parts Price List report and save CSV to output_path.
230
- Returns the path where the file was saved.
231
-
232
- Args:
233
- cookies_path: Path to cookies JSON (Chrome export). Defaults to REVOLUTIONNEXT_COOKIES_PATH or current dir.
234
- output_path: Where to save the CSV. Defaults to current dir / Parts_Price_List.csv.
235
- base_url: Revolution Next base URL (e.g. https://yoursite.revolutionnext.com.au). Defaults to env.
236
- """
237
- base_url = base_url or get_revnext_base_url_from_env()
238
- default_cookies = Path.cwd() / "revnext-cookies.json"
239
- cookies_path = Path(cookies_path) if cookies_path is not None else default_cookies
240
- output_path = Path(output_path) if output_path is not None else (Path.cwd() / "Parts_Price_List.csv")
241
- session = create_session(cookies_path, SERVICE_OBJECT, base_url)
242
- return run_report_flow(
243
- session,
244
- SERVICE_OBJECT,
245
- ACTIVITY_TAB_ID,
246
- _build_submit_body,
247
- output_path,
248
- base_url,
249
- post_submit_hook=_post_submit_closesubmit_factory(base_url),
250
- )
251
-
252
-
253
- if __name__ == "__main__":
254
- download_parts_price_list_report()
1
+ """
2
+ Download Parts Price List CSV report from Revolution Next (*.revolutionnext.com.au).
3
+ Uses cookies (Chrome export format) and configurable base URL.
4
+ """
5
+
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import requests
11
+
12
+ from revnext.common import create_session, run_report_flow
13
+ from revnext.config import get_revnext_base_url_from_env
14
+
15
+ SERVICE_OBJECT = "Revolution.Activity.IM.RPT.PartsPriceListPR"
16
+ ACTIVITY_TAB_ID = "N78b54de4_7cdc_43e0_9e42_71a49bec44f2"
17
+
18
+
19
+ def _build_submit_body() -> dict:
20
+ """Build the submitActivityTask request body. Use today's date for the trigger."""
21
+ tz = "+10:00"
22
+ now = datetime.now()
23
+ start_date = now.strftime("%Y-%m-%d")
24
+ start_time = f"{start_date}T{now.strftime('%H:%M')}:00.000{tz}"
25
+ return {
26
+ "_userContext_vg_coid": "03",
27
+ "_userContext_vg_divid": "1",
28
+ "_userContext_vg_dftdpt": "570",
29
+ "activityTabId": ACTIVITY_TAB_ID,
30
+ "dataSets": [
31
+ {
32
+ "name": "dsActivityTask",
33
+ "id": None,
34
+ "dataSet": {
35
+ "dsActivityTask": {
36
+ "prods:hasChanges": True,
37
+ "ttActivityTask": [
38
+ {
39
+ "prods:id": "ttActivityTask1945856",
40
+ "prods:rowState": "created",
41
+ "fldId": 1,
42
+ "taskID": "",
43
+ "loadRowidPassThrough": "dummy",
44
+ "executeActivityTaskNow": False,
45
+ "executedAt": None,
46
+ "logMessages": "",
47
+ "startTime": None,
48
+ "endTime": None,
49
+ }
50
+ ],
51
+ "prods:before": {},
52
+ }
53
+ },
54
+ },
55
+ {
56
+ "name": "dsActivityTaskTriggers",
57
+ "id": None,
58
+ "dataSet": {
59
+ "dsActivityTaskTriggers": {
60
+ "prods:hasChanges": True,
61
+ "ttActivityTaskTrigger": [
62
+ {
63
+ "prods:id": "ttActivityTaskTrigger1786112",
64
+ "prods:rowState": "created",
65
+ "fldId": 1,
66
+ "mode": "O",
67
+ "startDateTime": start_time,
68
+ "startDate": f"{start_date}T00:00:00.000{tz}",
69
+ "startHour": now.hour,
70
+ "startMinute": now.minute,
71
+ "recurEvery": 1,
72
+ "recurEveryUOM": "days",
73
+ "weeklySun": False,
74
+ "weeklyMon": False,
75
+ "weeklyTue": False,
76
+ "weeklyWed": False,
77
+ "weeklyThu": False,
78
+ "weeklyFri": False,
79
+ "weeklySat": False,
80
+ "monthsList": "",
81
+ "monthlyMode": "",
82
+ "monthlyDaysList": "",
83
+ "monthlyOnWeekNumber": "",
84
+ "monthlyOnDayOfWeek": "",
85
+ "triggerDescription": "",
86
+ "triggerNextSchedule": None,
87
+ "windowTimeFrom": "",
88
+ "windowTimeTo": "",
89
+ "windowAllDay": False,
90
+ }
91
+ ],
92
+ "prods:before": {},
93
+ }
94
+ },
95
+ },
96
+ {
97
+ "name": "dsParams",
98
+ "id": None,
99
+ "dataSet": {
100
+ "dsParams": {
101
+ "prods:hasChanges": True,
102
+ "tt_params": [
103
+ {
104
+ "prods:id": "tt_paramsFldId1",
105
+ "prods:rowState": "modified",
106
+ "fldid": 1,
107
+ "coid": "03",
108
+ "divid": "1",
109
+ "activityid": "IM.RPT.PartsPriceListPR",
110
+ "taskid": "",
111
+ "tasksts": None,
112
+ "rptid": "",
113
+ "pdf": "",
114
+ "formprt": False,
115
+ "csvout": True,
116
+ "emailopt": "n",
117
+ "emailme": False,
118
+ "useremail": "lwarneke@mikecarneytoyota.com.au",
119
+ "emailprinter": False,
120
+ "prtid": "",
121
+ "ddpflg": False,
122
+ "ddpquo": 0,
123
+ "submitopt": "p",
124
+ "email_staff": False,
125
+ "staff_email": "",
126
+ "email_other": False,
127
+ "other_email": "",
128
+ "subject": "Parts Price List",
129
+ "attn": "",
130
+ "email_text": "",
131
+ "email_signature": "",
132
+ "email_sig_type": "D",
133
+ "prttyp": "s",
134
+ "dptid": "570",
135
+ "frnid": "",
136
+ "frnidto": "",
137
+ "binid": "",
138
+ "binidto": "",
139
+ "prctyp1": "L",
140
+ "prctyp2": "S",
141
+ "incgst1": True,
142
+ "incgst2": True,
143
+ "exportexcel": False,
144
+ "tasktype": "",
145
+ }
146
+ ],
147
+ "prods:before": {
148
+ "tt_params": [
149
+ {
150
+ "prods:id": "tt_paramsFldId1",
151
+ "prods:rowState": "modified",
152
+ "fldid": 1,
153
+ "coid": "03",
154
+ "divid": "1",
155
+ "activityid": "",
156
+ "taskid": "",
157
+ "tasksts": None,
158
+ "rptid": "",
159
+ "pdf": "",
160
+ "formprt": False,
161
+ "csvout": False,
162
+ "emailopt": "",
163
+ "emailme": False,
164
+ "useremail": "",
165
+ "emailprinter": False,
166
+ "prtid": "",
167
+ "ddpflg": False,
168
+ "ddpquo": 0,
169
+ "submitopt": "",
170
+ "email_staff": False,
171
+ "staff_email": "",
172
+ "email_other": False,
173
+ "other_email": "",
174
+ "subject": "",
175
+ "attn": "",
176
+ "email_text": "",
177
+ "email_signature": "",
178
+ "email_sig_type": "",
179
+ "prttyp": "s",
180
+ "dptid": "130",
181
+ "frnid": "",
182
+ "frnidto": "",
183
+ "binid": "",
184
+ "binidto": "",
185
+ "prctyp1": "",
186
+ "prctyp2": "",
187
+ "incgst1": False,
188
+ "incgst2": False,
189
+ "exportexcel": False,
190
+ "tasktype": "",
191
+ }
192
+ ]
193
+ },
194
+ }
195
+ },
196
+ },
197
+ ],
198
+ "stopOnWarning": True,
199
+ "validateOnly": False,
200
+ "uiType": "ISC",
201
+ }
202
+
203
+
204
+ def _post_submit_closesubmit_factory(base_url: str):
205
+ """Return a hook that calls onChoose_btn_closesubmit (required for Parts Price List flow)."""
206
+ def _post_submit_closesubmit(session: requests.Session) -> None:
207
+ url = f"{base_url}/next/rest/si/presenter/onChoose_btn_closesubmit"
208
+ body = {
209
+ "_userContext_vg_coid": "03",
210
+ "_userContext_vg_divid": "1",
211
+ "_userContext_vg_dftdpt": "570",
212
+ "activityTabId": ACTIVITY_TAB_ID,
213
+ "ctrlProp": [
214
+ {"name": "tt_params.submitopt", "prop": "SCREENVALUE", "value": "p"}
215
+ ],
216
+ "uiType": "ISC",
217
+ }
218
+ r = session.post(url, json=body)
219
+ r.raise_for_status()
220
+ return _post_submit_closesubmit
221
+
222
+
223
+ def download_parts_price_list_report(
224
+ cookies_path: Optional[Path | str] = None,
225
+ output_path: Optional[Path | str] = None,
226
+ base_url: Optional[str] = None,
227
+ ) -> Path:
228
+ """
229
+ Run the Parts Price List report and save CSV to output_path.
230
+ Returns the path where the file was saved.
231
+
232
+ Args:
233
+ cookies_path: Path to cookies JSON (Chrome export). Defaults to REVOLUTIONNEXT_COOKIES_PATH or current dir.
234
+ output_path: Where to save the CSV. Defaults to current dir / Parts_Price_List.csv.
235
+ base_url: Revolution Next base URL (e.g. https://yoursite.revolutionnext.com.au). Defaults to env.
236
+ """
237
+ base_url = base_url or get_revnext_base_url_from_env()
238
+ default_cookies = Path.cwd() / "revnext-cookies.json"
239
+ cookies_path = Path(cookies_path) if cookies_path is not None else default_cookies
240
+ output_path = Path(output_path) if output_path is not None else (Path.cwd() / "Parts_Price_List.csv")
241
+ session = create_session(cookies_path, SERVICE_OBJECT, base_url)
242
+ return run_report_flow(
243
+ session,
244
+ SERVICE_OBJECT,
245
+ ACTIVITY_TAB_ID,
246
+ _build_submit_body,
247
+ output_path,
248
+ base_url,
249
+ post_submit_hook=_post_submit_closesubmit_factory(base_url),
250
+ )
251
+
252
+
253
+ if __name__ == "__main__":
254
+ download_parts_price_list_report()
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: revnext
3
+ Version: 0.1.2
4
+ Summary: Revolution Next (*.revolutionnext.com.au) report downloads via REST API
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/Luen/RevNext-TUNE/
7
+ Project-URL: Repository, https://github.com/Luen/RevNext-TUNE/
8
+ Keywords: revnext,revolution-next,reports,api
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: requests
18
+
19
+ # revnext
20
+
21
+ Download Revolution Next (*.revolutionnext.com.au) reports (Parts Price List, Parts by Bin Location) via REST API using cookies/session.
22
+
23
+ Install: `pip install revnext`
24
+ Or from repo root: `pip install -e ./packages/revnext`
25
+
26
+ ## Quick start
27
+
28
+ You need a cookies file (Chrome export for your RevNext domain). Then call the download functions with your instance URL and download location.
29
+
30
+ ### Download one report with a specific file path
31
+
32
+ ```python
33
+ from pathlib import Path
34
+ from revnext import download_parts_by_bin_report, download_parts_price_list_report
35
+
36
+ base_url = "https://yoursite.revolutionnext.com.au"
37
+ cookies_path = Path("revnext-cookies.json")
38
+
39
+ # Parts by Bin Location
40
+ path1 = download_parts_by_bin_report(
41
+ base_url=base_url,
42
+ output_path=Path("C:/Reports/parts_by_bin.csv"),
43
+ cookies_path=cookies_path,
44
+ )
45
+
46
+ # Parts Price List
47
+ path2 = download_parts_price_list_report(
48
+ base_url=base_url,
49
+ output_path=Path("C:/Reports/parts_price_list.csv"),
50
+ cookies_path=cookies_path,
51
+ )
52
+ ```
53
+
54
+ ### Example: download both reports (implement in your project)
55
+
56
+ Copy this into your project to run both reports in sequence:
57
+
58
+ ```python
59
+ from pathlib import Path
60
+ from typing import Optional
61
+
62
+ from revnext import download_parts_by_bin_report, download_parts_price_list_report
63
+
64
+
65
+ def download_all_reports(
66
+ cookies_path: Optional[Path | str] = None,
67
+ output_dir: Optional[Path | str] = None,
68
+ base_url: Optional[str] = None,
69
+ ) -> list[Path]:
70
+ """Run both reports and save CSVs. Returns the list of paths where files were saved."""
71
+ output_dir = Path(output_dir) if output_dir is not None else Path.cwd()
72
+ path1 = download_parts_by_bin_report(
73
+ cookies_path=cookies_path,
74
+ output_path=output_dir / "Parts_By_Bin_Location.csv",
75
+ base_url=base_url,
76
+ )
77
+ path2 = download_parts_price_list_report(
78
+ cookies_path=cookies_path,
79
+ output_path=output_dir / "Parts_Price_List.csv",
80
+ base_url=base_url,
81
+ )
82
+ return [path1, path2]
83
+ ```
84
+
85
+ ### Example: download both reports in parallel (implement in your project)
86
+
87
+ Copy this into your project to run both reports concurrently (Promise.all-style):
88
+
89
+ ```python
90
+ from concurrent.futures import ThreadPoolExecutor
91
+ from pathlib import Path
92
+ from typing import Optional
93
+
94
+ from revnext import download_parts_by_bin_report, download_parts_price_list_report
95
+
96
+
97
+ def download_all_reports_parallel(
98
+ cookies_path: Optional[Path | str] = None,
99
+ output_dir: Optional[Path | str] = None,
100
+ base_url: Optional[str] = None,
101
+ ) -> list[Path]:
102
+ """Run both reports in parallel. Returns the list of paths where files were saved."""
103
+ output_dir = Path(output_dir) if output_dir is not None else Path.cwd()
104
+ path1 = output_dir / "Parts_By_Bin_Location.csv"
105
+ path2 = output_dir / "Parts_Price_List.csv"
106
+ with ThreadPoolExecutor(max_workers=2) as executor:
107
+ f1 = executor.submit(
108
+ download_parts_by_bin_report,
109
+ cookies_path=cookies_path,
110
+ output_path=path1,
111
+ base_url=base_url,
112
+ )
113
+ f2 = executor.submit(
114
+ download_parts_price_list_report,
115
+ cookies_path=cookies_path,
116
+ output_path=path2,
117
+ base_url=base_url,
118
+ )
119
+ return [f1.result(), f2.result()]
120
+ ```
121
+
122
+ ### Using environment variables
123
+
124
+ Set `REVOLUTIONNEXT_URL` and optionally `REVOLUTIONNEXT_COOKIES_PATH`; then you can omit `base_url` and `cookies_path` in code.
125
+
126
+ See the [main repo README](../README.md) for full configuration options.
@@ -0,0 +1,9 @@
1
+ revnext/__init__.py,sha256=dkXhuM6Da2dqUr-nzacfPfeEhDnTvoW12ewDdpZbE8M,472
2
+ revnext/common.py,sha256=PkF747ILSdxRaVhj7Putp6bjSjqWzgd3LvtxAiddXqc,6944
3
+ revnext/config.py,sha256=twdxnQn_w3nii-ovG22Cy1E4FRe5ephLh9sZAkMm9Yo,1978
4
+ revnext/parts_by_bin_report.py,sha256=j8aCVMY-8XQExlBpbGWEkueCyiyowHNapnEFQO1JPeU,11158
5
+ revnext/parts_price_list_report.py,sha256=B5fq1lBJt_ttG1WfPaMWt8dm-AstNlLEL62TMvzlFC0,10951
6
+ revnext-0.1.2.dist-info/METADATA,sha256=FHRPfasfl8uTsIS67i0MK_ntuwLP-uHfxp6x_BFcLps,4222
7
+ revnext-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
8
+ revnext-0.1.2.dist-info/top_level.txt,sha256=fW_mpOHYljzK-IPZOiJBGDIWkMmcbsjWzqjot5KDUwk,8
9
+ revnext-0.1.2.dist-info/RECORD,,