revnext 0.1.0__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.
- revnext-0.1.0/PKG-INFO +24 -0
- revnext-0.1.0/README.md +8 -0
- revnext-0.1.0/pyproject.toml +27 -0
- revnext-0.1.0/revnext/__init__.py +18 -0
- revnext-0.1.0/revnext/common.py +196 -0
- revnext-0.1.0/revnext/config.py +61 -0
- revnext-0.1.0/revnext/download_all_reports.py +49 -0
- revnext-0.1.0/revnext/parts_by_bin_report.py +250 -0
- revnext-0.1.0/revnext/parts_price_list_report.py +254 -0
- revnext-0.1.0/revnext.egg-info/PKG-INFO +24 -0
- revnext-0.1.0/revnext.egg-info/SOURCES.txt +13 -0
- revnext-0.1.0/revnext.egg-info/dependency_links.txt +1 -0
- revnext-0.1.0/revnext.egg-info/requires.txt +1 -0
- revnext-0.1.0/revnext.egg-info/top_level.txt +1 -0
- revnext-0.1.0/setup.cfg +4 -0
revnext-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revnext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Revolution Next (*.revolutionnext.com.au) report downloads via REST API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: revnext,revolution-next,reports,api
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
|
|
17
|
+
# revnext
|
|
18
|
+
|
|
19
|
+
Download Revolution Next (*.revolutionnext.com.au) reports (Parts Price List, Parts by Bin Location) via REST API using cookies/session.
|
|
20
|
+
|
|
21
|
+
Install: `pip install revnext`
|
|
22
|
+
Or from repo root: `pip install -e ./packages/revnext`
|
|
23
|
+
|
|
24
|
+
See the [main repo README](../README.md) for configuration and usage.
|
revnext-0.1.0/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# revnext
|
|
2
|
+
|
|
3
|
+
Download Revolution Next (*.revolutionnext.com.au) reports (Parts Price List, Parts by Bin Location) via REST API using cookies/session.
|
|
4
|
+
|
|
5
|
+
Install: `pip install revnext`
|
|
6
|
+
Or from repo root: `pip install -e ./packages/revnext`
|
|
7
|
+
|
|
8
|
+
See the [main repo README](../README.md) for configuration and usage.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "revnext"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Revolution Next (*.revolutionnext.com.au) report downloads via REST API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["revnext", "revolution-next", "reports", "api"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"requests",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["."]
|
|
27
|
+
include = ["revnext*"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Revolution Next (*.revolutionnext.com.au) report downloads via REST API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from revnext.config import RevNextConfig, get_revnext_base_url_from_env
|
|
6
|
+
from revnext.parts_by_bin_report import download_parts_by_bin_report
|
|
7
|
+
from revnext.parts_price_list_report import download_parts_price_list_report
|
|
8
|
+
from revnext.download_all_reports import download_all_reports
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"RevNextConfig",
|
|
12
|
+
"get_revnext_base_url_from_env",
|
|
13
|
+
"download_parts_by_bin_report",
|
|
14
|
+
"download_parts_price_list_report",
|
|
15
|
+
"download_all_reports",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared utilities for Revolution Next (*.revolutionnext.com.au) report downloads.
|
|
3
|
+
Cookie loading, session creation, and generic submit → poll → loadData → download flow.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _common_headers(base_url: str) -> dict:
|
|
16
|
+
"""Build common request headers for the given base URL."""
|
|
17
|
+
return {
|
|
18
|
+
"accept": "*/*",
|
|
19
|
+
"accept-language": "en-AU,en-US;q=0.9,en-GB;q=0.8,en;q=0.7",
|
|
20
|
+
"content-type": "application/json; charset=UTF-8",
|
|
21
|
+
"origin": base_url,
|
|
22
|
+
"referrer": f"{base_url}/next/Fluid.html?useTabs",
|
|
23
|
+
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="144", "Google Chrome";v="144"',
|
|
24
|
+
"sec-ch-ua-mobile": "?0",
|
|
25
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
26
|
+
"sec-fetch-dest": "empty",
|
|
27
|
+
"sec-fetch-mode": "cors",
|
|
28
|
+
"sec-fetch-site": "same-origin",
|
|
29
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_cookies_for_domain(cookies_path: Path, domain: str) -> list[tuple[str, str]]:
|
|
34
|
+
"""Load cookies from Chrome-export JSON and return (name, value) pairs for the domain."""
|
|
35
|
+
with open(cookies_path, encoding="utf-8") as f:
|
|
36
|
+
raw = json.load(f)
|
|
37
|
+
pairs = []
|
|
38
|
+
for c in raw:
|
|
39
|
+
d = c.get("domain", "")
|
|
40
|
+
if not d:
|
|
41
|
+
continue
|
|
42
|
+
if domain == d or domain.endswith(d.lstrip(".")):
|
|
43
|
+
pairs.append((c["name"], c["value"]))
|
|
44
|
+
return pairs
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cookie_header(pairs: list[tuple[str, str]]) -> str:
|
|
48
|
+
"""Build Cookie header value from (name, value) pairs."""
|
|
49
|
+
return "; ".join(f"{name}={value}" for name, value in pairs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_session(
|
|
53
|
+
cookies_path: Path,
|
|
54
|
+
service_object: str,
|
|
55
|
+
base_url: str,
|
|
56
|
+
) -> requests.Session:
|
|
57
|
+
"""Create a requests Session with cookies and common headers for the given base URL and service object."""
|
|
58
|
+
parsed = urlparse(base_url)
|
|
59
|
+
domain = parsed.netloc or parsed.path
|
|
60
|
+
if not domain:
|
|
61
|
+
raise ValueError(f"Invalid base_url: {base_url}")
|
|
62
|
+
cookie_pairs = load_cookies_for_domain(cookies_path, domain)
|
|
63
|
+
if not cookie_pairs:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"No cookies found for {domain}. Export cookies for this domain (e.g. revnext-cookies.json)."
|
|
66
|
+
)
|
|
67
|
+
session = requests.Session()
|
|
68
|
+
session.headers.update(_common_headers(base_url))
|
|
69
|
+
session.headers["cookie"] = cookie_header(cookie_pairs)
|
|
70
|
+
session.headers["x-service-object"] = service_object
|
|
71
|
+
return session
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_task_id(submit_response: dict) -> str | None:
|
|
75
|
+
"""Extract taskID from submitActivityTask response."""
|
|
76
|
+
for ds in submit_response.get("dataSets", []):
|
|
77
|
+
if ds.get("name") != "dsActivityTask":
|
|
78
|
+
continue
|
|
79
|
+
d = ds.get("dataSet", {}).get("dsActivityTask", {})
|
|
80
|
+
for row in d.get("ttActivityTask", []):
|
|
81
|
+
tid = row.get("taskID")
|
|
82
|
+
if tid:
|
|
83
|
+
return tid
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_poll_done(poll_response: dict) -> bool:
|
|
88
|
+
"""True when report is ready (autoPollResponse returns -1)."""
|
|
89
|
+
for cp in poll_response.get("ctrlProp", []):
|
|
90
|
+
if cp.get("name") == "button.autoPollResponse" and cp.get("value") == "-1":
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_response_url_from_load_data(load_data: dict) -> str | None:
|
|
96
|
+
"""Extract responseUrl from loadData response (ttActivityTaskResponse)."""
|
|
97
|
+
for ds in load_data.get("dataSets", []):
|
|
98
|
+
if ds.get("name") != "dsActivityTask":
|
|
99
|
+
continue
|
|
100
|
+
d = ds.get("dataSet", {}).get("dsActivityTask", {})
|
|
101
|
+
for row in d.get("ttActivityTaskResponse", []):
|
|
102
|
+
url = row.get("responseUrl")
|
|
103
|
+
if url:
|
|
104
|
+
return url
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_report_flow(
|
|
109
|
+
session: requests.Session,
|
|
110
|
+
service_object: str,
|
|
111
|
+
activity_tab_id: str,
|
|
112
|
+
get_submit_body: Callable[[], dict],
|
|
113
|
+
output_path: Path,
|
|
114
|
+
base_url: str,
|
|
115
|
+
post_submit_hook: Callable[[requests.Session], None] | None = None,
|
|
116
|
+
max_polls: int = 60,
|
|
117
|
+
poll_interval: float = 2,
|
|
118
|
+
) -> Path:
|
|
119
|
+
"""
|
|
120
|
+
Submit report task, poll until ready, loadData for download URL, then download CSV.
|
|
121
|
+
Optionally call post_submit_hook(session) after submit (e.g. onChoose_btn_closesubmit).
|
|
122
|
+
Returns the path where the file was saved.
|
|
123
|
+
"""
|
|
124
|
+
submit_url = f"{base_url}/next/rest/si/static/submitActivityTask"
|
|
125
|
+
r = session.post(submit_url, json=get_submit_body())
|
|
126
|
+
r.raise_for_status()
|
|
127
|
+
submit_data = r.json()
|
|
128
|
+
if not submit_data.get("submittedSuccess"):
|
|
129
|
+
raise RuntimeError("submitActivityTask did not report success.")
|
|
130
|
+
|
|
131
|
+
task_id = extract_task_id(submit_data)
|
|
132
|
+
if not task_id:
|
|
133
|
+
raise RuntimeError("Could not get taskID from submit response.")
|
|
134
|
+
print(f"Task submitted: {task_id}")
|
|
135
|
+
|
|
136
|
+
if post_submit_hook:
|
|
137
|
+
post_submit_hook(session)
|
|
138
|
+
|
|
139
|
+
poll_url = f"{base_url}/next/rest/si/presenter/autoPollResponse"
|
|
140
|
+
poll_body = {
|
|
141
|
+
"_userContext_vg_coid": "03",
|
|
142
|
+
"_userContext_vg_divid": "1",
|
|
143
|
+
"_userContext_vg_dftdpt": "570",
|
|
144
|
+
"activityTabId": activity_tab_id,
|
|
145
|
+
"ctrlProp": [
|
|
146
|
+
{"name": "ttActivityTask.taskID", "prop": "SCREENVALUE", "value": task_id}
|
|
147
|
+
],
|
|
148
|
+
"uiType": "ISC",
|
|
149
|
+
}
|
|
150
|
+
for i in range(max_polls):
|
|
151
|
+
time.sleep(poll_interval)
|
|
152
|
+
r = session.post(poll_url, json=poll_body)
|
|
153
|
+
r.raise_for_status()
|
|
154
|
+
poll_data = r.json()
|
|
155
|
+
if is_poll_done(poll_data):
|
|
156
|
+
print("Report generation complete.")
|
|
157
|
+
break
|
|
158
|
+
print(f" Poll {i + 1}: still generating...")
|
|
159
|
+
else:
|
|
160
|
+
raise RuntimeError("Timed out waiting for report.")
|
|
161
|
+
|
|
162
|
+
load_url = f"{base_url}/next/rest/si/static/loadData"
|
|
163
|
+
load_body = {
|
|
164
|
+
"taskID": task_id,
|
|
165
|
+
"ctrlProp": [{"name": "dummy", "prop": "LOADDATA", "value": "dummy"}],
|
|
166
|
+
"parentActivity": None,
|
|
167
|
+
"historyID": "self,dummy,dummy",
|
|
168
|
+
"tabID": "self",
|
|
169
|
+
"activityType": "dummy",
|
|
170
|
+
"fluidService": "dummy",
|
|
171
|
+
"uiType": "ISC",
|
|
172
|
+
"_userContext_vg_coid": "03",
|
|
173
|
+
"_userContext_vg_divid": "1",
|
|
174
|
+
"_userContext_vg_dftdpt": "570",
|
|
175
|
+
"activityTabId": activity_tab_id,
|
|
176
|
+
"loadMode": "EDIT",
|
|
177
|
+
"loadRowid": "dummy",
|
|
178
|
+
}
|
|
179
|
+
r = session.post(load_url, json=load_body)
|
|
180
|
+
r.raise_for_status()
|
|
181
|
+
load_data = r.json()
|
|
182
|
+
|
|
183
|
+
response_url = get_response_url_from_load_data(load_data)
|
|
184
|
+
if not response_url:
|
|
185
|
+
raise RuntimeError("Could not get responseUrl from loadData.")
|
|
186
|
+
|
|
187
|
+
if not response_url.startswith("http"):
|
|
188
|
+
response_url = f"{base_url}/next/{response_url.lstrip('/')}"
|
|
189
|
+
print(f"Download URL: {response_url}")
|
|
190
|
+
|
|
191
|
+
r = session.get(response_url)
|
|
192
|
+
r.raise_for_status()
|
|
193
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
output_path.write_bytes(r.content)
|
|
195
|
+
print(f"Saved: {output_path}")
|
|
196
|
+
return output_path
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration for Revolution Next (*.revolutionnext.com.au) report downloads.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _load_dotenv_if_available() -> None:
|
|
12
|
+
try:
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
load_dotenv()
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class RevNextConfig:
|
|
21
|
+
"""Configuration for Revolution Next (*.revolutionnext.com.au) API / report downloads."""
|
|
22
|
+
|
|
23
|
+
base_url: str
|
|
24
|
+
cookies_path: Optional[Path] = None
|
|
25
|
+
username: Optional[str] = None
|
|
26
|
+
password: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_env(
|
|
30
|
+
cls,
|
|
31
|
+
*,
|
|
32
|
+
base_url: Optional[str] = None,
|
|
33
|
+
cookies_path: Optional[Path] = None,
|
|
34
|
+
username: Optional[str] = None,
|
|
35
|
+
password: Optional[str] = None,
|
|
36
|
+
load_dotenv: bool = True,
|
|
37
|
+
) -> "RevNextConfig":
|
|
38
|
+
"""Build config from environment variables. Override any field by passing it explicitly."""
|
|
39
|
+
if load_dotenv:
|
|
40
|
+
_load_dotenv_if_available()
|
|
41
|
+
url = base_url or os.getenv("REVOLUTIONNEXT_URL") or "https://mikecarney.revolutionnext.com.au"
|
|
42
|
+
if url and not url.startswith(("http://", "https://")):
|
|
43
|
+
url = "https://" + url
|
|
44
|
+
cp = cookies_path
|
|
45
|
+
if cp is None and os.getenv("REVOLUTIONNEXT_COOKIES_PATH"):
|
|
46
|
+
cp = Path(os.getenv("REVOLUTIONNEXT_COOKIES_PATH"))
|
|
47
|
+
return cls(
|
|
48
|
+
base_url=url,
|
|
49
|
+
cookies_path=cp,
|
|
50
|
+
username=username or os.getenv("REVOLUTIONNEXT_USERNAME"),
|
|
51
|
+
password=password or os.getenv("REVOLUTIONNEXT_PASSWORD"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_revnext_base_url_from_env() -> str:
|
|
56
|
+
"""Return RevNext base URL from environment (REVOLUTIONNEXT_URL)."""
|
|
57
|
+
_load_dotenv_if_available()
|
|
58
|
+
url = os.getenv("REVOLUTIONNEXT_URL") or "https://mikecarney.revolutionnext.com.au"
|
|
59
|
+
if not url.startswith(("http://", "https://")):
|
|
60
|
+
url = "https://" + url
|
|
61
|
+
return url
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Download both Revolution Next CSV reports (Parts By Bin Location and Parts Price List)
|
|
3
|
+
using cookies and optional config.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from revnext.parts_by_bin_report import download_parts_by_bin_report
|
|
10
|
+
from revnext.parts_price_list_report import download_parts_price_list_report
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def download_all_reports(
|
|
14
|
+
cookies_path: Optional[Path | str] = None,
|
|
15
|
+
output_dir: Optional[Path | str] = None,
|
|
16
|
+
base_url: Optional[str] = None,
|
|
17
|
+
) -> list[Path]:
|
|
18
|
+
"""
|
|
19
|
+
Run both Parts By Bin Location and Parts Price List reports and save CSVs.
|
|
20
|
+
Returns the list of paths where files were saved.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
cookies_path: Path to cookies JSON. Defaults to current dir / revnext-cookies.json.
|
|
24
|
+
output_dir: Directory for output CSVs. Defaults to current directory.
|
|
25
|
+
base_url: Revolution Next base URL. Defaults to REVOLUTIONNEXT_URL env or default.
|
|
26
|
+
"""
|
|
27
|
+
output_dir = Path(output_dir) if output_dir is not None else Path.cwd()
|
|
28
|
+
|
|
29
|
+
saved: list[Path] = []
|
|
30
|
+
print("--- Parts By Bin Location ---")
|
|
31
|
+
path1 = download_parts_by_bin_report(
|
|
32
|
+
cookies_path=cookies_path,
|
|
33
|
+
output_path=output_dir / "Parts_By_Bin_Location.csv",
|
|
34
|
+
base_url=base_url,
|
|
35
|
+
)
|
|
36
|
+
saved.append(path1)
|
|
37
|
+
print("\n--- Parts Price List ---")
|
|
38
|
+
path2 = download_parts_price_list_report(
|
|
39
|
+
cookies_path=cookies_path,
|
|
40
|
+
output_path=output_dir / "Parts_Price_List.csv",
|
|
41
|
+
base_url=base_url,
|
|
42
|
+
)
|
|
43
|
+
saved.append(path2)
|
|
44
|
+
print(f"\nDone. Saved {len(saved)} file(s).")
|
|
45
|
+
return saved
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
download_all_reports()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Download Parts By Bin Location 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
|
+
from revnext.common import create_session, run_report_flow
|
|
11
|
+
from revnext.config import get_revnext_base_url_from_env
|
|
12
|
+
|
|
13
|
+
SERVICE_OBJECT = "Revolution.Activity.IM.RPT.PartsByBinLocationPR"
|
|
14
|
+
ACTIVITY_TAB_ID = "Nce9eac79_528b_4fc4_a294_b055a6dde16b"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_submit_body() -> dict:
|
|
18
|
+
"""Build the submitActivityTask request body. Use today's date for the trigger."""
|
|
19
|
+
tz = "+10:00"
|
|
20
|
+
now = datetime.now()
|
|
21
|
+
start_date = now.strftime("%Y-%m-%d")
|
|
22
|
+
start_time = f"{start_date}T{now.strftime('%H:%M')}:00.000{tz}"
|
|
23
|
+
return {
|
|
24
|
+
"_userContext_vg_coid": "03",
|
|
25
|
+
"_userContext_vg_divid": "1",
|
|
26
|
+
"_userContext_vg_dftdpt": "570",
|
|
27
|
+
"activityTabId": ACTIVITY_TAB_ID,
|
|
28
|
+
"dataSets": [
|
|
29
|
+
{
|
|
30
|
+
"name": "dsActivityTask",
|
|
31
|
+
"id": None,
|
|
32
|
+
"dataSet": {
|
|
33
|
+
"dsActivityTask": {
|
|
34
|
+
"prods:hasChanges": True,
|
|
35
|
+
"ttActivityTask": [
|
|
36
|
+
{
|
|
37
|
+
"prods:id": "ttActivityTask1827072",
|
|
38
|
+
"prods:rowState": "created",
|
|
39
|
+
"fldId": 1,
|
|
40
|
+
"taskID": "",
|
|
41
|
+
"loadRowidPassThrough": "dummy",
|
|
42
|
+
"executeActivityTaskNow": False,
|
|
43
|
+
"executedAt": None,
|
|
44
|
+
"logMessages": "",
|
|
45
|
+
"startTime": None,
|
|
46
|
+
"endTime": None,
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"prods:before": {},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "dsActivityTaskTriggers",
|
|
55
|
+
"id": None,
|
|
56
|
+
"dataSet": {
|
|
57
|
+
"dsActivityTaskTriggers": {
|
|
58
|
+
"prods:hasChanges": True,
|
|
59
|
+
"ttActivityTaskTrigger": [
|
|
60
|
+
{
|
|
61
|
+
"prods:id": "ttActivityTaskTrigger1972480",
|
|
62
|
+
"prods:rowState": "created",
|
|
63
|
+
"fldId": 1,
|
|
64
|
+
"mode": "O",
|
|
65
|
+
"startDateTime": start_time,
|
|
66
|
+
"startDate": f"{start_date}T00:00:00.000{tz}",
|
|
67
|
+
"startHour": now.hour,
|
|
68
|
+
"startMinute": now.minute,
|
|
69
|
+
"recurEvery": 1,
|
|
70
|
+
"recurEveryUOM": "days",
|
|
71
|
+
"weeklySun": False,
|
|
72
|
+
"weeklyMon": False,
|
|
73
|
+
"weeklyTue": False,
|
|
74
|
+
"weeklyWed": False,
|
|
75
|
+
"weeklyThu": False,
|
|
76
|
+
"weeklyFri": False,
|
|
77
|
+
"weeklySat": False,
|
|
78
|
+
"monthsList": "",
|
|
79
|
+
"monthlyMode": "",
|
|
80
|
+
"monthlyDaysList": "",
|
|
81
|
+
"monthlyOnWeekNumber": "",
|
|
82
|
+
"monthlyOnDayOfWeek": "",
|
|
83
|
+
"triggerDescription": "",
|
|
84
|
+
"triggerNextSchedule": None,
|
|
85
|
+
"windowTimeFrom": "",
|
|
86
|
+
"windowTimeTo": "",
|
|
87
|
+
"windowAllDay": False,
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"prods:before": {},
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "dsParams",
|
|
96
|
+
"id": None,
|
|
97
|
+
"dataSet": {
|
|
98
|
+
"dsParams": {
|
|
99
|
+
"prods:hasChanges": True,
|
|
100
|
+
"tt_params": [
|
|
101
|
+
{
|
|
102
|
+
"prods:id": "tt_paramsFldId1",
|
|
103
|
+
"prods:rowState": "modified",
|
|
104
|
+
"fldid": 1,
|
|
105
|
+
"coid": "03",
|
|
106
|
+
"divid": "1",
|
|
107
|
+
"activityid": "IM.RPT.PartsByBinLocationPR",
|
|
108
|
+
"taskid": "",
|
|
109
|
+
"tasksts": None,
|
|
110
|
+
"rptid": "",
|
|
111
|
+
"pdf": "",
|
|
112
|
+
"formprt": False,
|
|
113
|
+
"csvout": True,
|
|
114
|
+
"emailopt": "n",
|
|
115
|
+
"emailme": False,
|
|
116
|
+
"useremail": "lwarneke@mikecarneytoyota.com.au",
|
|
117
|
+
"emailprinter": False,
|
|
118
|
+
"prtid": "",
|
|
119
|
+
"ddpflg": False,
|
|
120
|
+
"ddpquo": 0,
|
|
121
|
+
"submitopt": "p",
|
|
122
|
+
"email_staff": False,
|
|
123
|
+
"staff_email": "",
|
|
124
|
+
"email_other": False,
|
|
125
|
+
"other_email": "",
|
|
126
|
+
"subject": "Parts By Bin Location",
|
|
127
|
+
"attn": "",
|
|
128
|
+
"email_text": "",
|
|
129
|
+
"email_signature": "",
|
|
130
|
+
"email_sig_type": "D",
|
|
131
|
+
"frmdptid": "570",
|
|
132
|
+
"todptid": "570",
|
|
133
|
+
"frmfrnid": "",
|
|
134
|
+
"tofrnid": "",
|
|
135
|
+
"frmbinid": "",
|
|
136
|
+
"tobinid": "",
|
|
137
|
+
"frmmovecode": "",
|
|
138
|
+
"tomovecode": "",
|
|
139
|
+
"stktyp": "A",
|
|
140
|
+
"prntnotzero": False,
|
|
141
|
+
"prntzero": False,
|
|
142
|
+
"prntstkzero": False,
|
|
143
|
+
"noprimarybin": False,
|
|
144
|
+
"noalternatebin": False,
|
|
145
|
+
"hasalternateonly": False,
|
|
146
|
+
"bothprimaryalternate": False,
|
|
147
|
+
"lastsaledate": None,
|
|
148
|
+
"lastreceiptdate": None,
|
|
149
|
+
"prntavgcost": False,
|
|
150
|
+
"rptformat": "N",
|
|
151
|
+
"tasktype": "",
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"prods:before": {
|
|
155
|
+
"tt_params": [
|
|
156
|
+
{
|
|
157
|
+
"prods:id": "tt_paramsFldId1",
|
|
158
|
+
"prods:rowState": "modified",
|
|
159
|
+
"fldid": 1,
|
|
160
|
+
"coid": "03",
|
|
161
|
+
"divid": "1",
|
|
162
|
+
"activityid": "",
|
|
163
|
+
"taskid": "",
|
|
164
|
+
"tasksts": None,
|
|
165
|
+
"rptid": "",
|
|
166
|
+
"pdf": "",
|
|
167
|
+
"formprt": False,
|
|
168
|
+
"csvout": False,
|
|
169
|
+
"emailopt": "",
|
|
170
|
+
"emailme": False,
|
|
171
|
+
"useremail": "",
|
|
172
|
+
"emailprinter": False,
|
|
173
|
+
"prtid": "",
|
|
174
|
+
"ddpflg": False,
|
|
175
|
+
"ddpquo": 0,
|
|
176
|
+
"submitopt": "",
|
|
177
|
+
"email_staff": False,
|
|
178
|
+
"staff_email": "",
|
|
179
|
+
"email_other": False,
|
|
180
|
+
"other_email": "",
|
|
181
|
+
"subject": "",
|
|
182
|
+
"attn": "",
|
|
183
|
+
"email_text": "",
|
|
184
|
+
"email_signature": "",
|
|
185
|
+
"email_sig_type": "",
|
|
186
|
+
"frmdptid": "130",
|
|
187
|
+
"todptid": "130",
|
|
188
|
+
"frmfrnid": "",
|
|
189
|
+
"tofrnid": "",
|
|
190
|
+
"frmbinid": "",
|
|
191
|
+
"tobinid": "",
|
|
192
|
+
"frmmovecode": "",
|
|
193
|
+
"tomovecode": "",
|
|
194
|
+
"stktyp": "P",
|
|
195
|
+
"prntnotzero": False,
|
|
196
|
+
"prntzero": False,
|
|
197
|
+
"prntstkzero": False,
|
|
198
|
+
"noprimarybin": False,
|
|
199
|
+
"noalternatebin": False,
|
|
200
|
+
"hasalternateonly": False,
|
|
201
|
+
"bothprimaryalternate": False,
|
|
202
|
+
"lastsaledate": None,
|
|
203
|
+
"lastreceiptdate": None,
|
|
204
|
+
"prntavgcost": False,
|
|
205
|
+
"rptformat": "",
|
|
206
|
+
"tasktype": "",
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
"stopOnWarning": True,
|
|
215
|
+
"validateOnly": False,
|
|
216
|
+
"uiType": "ISC",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def download_parts_by_bin_report(
|
|
221
|
+
cookies_path: Optional[Path | str] = None,
|
|
222
|
+
output_path: Optional[Path | str] = None,
|
|
223
|
+
base_url: Optional[str] = None,
|
|
224
|
+
) -> Path:
|
|
225
|
+
"""
|
|
226
|
+
Run the Parts By Bin Location report and save CSV to output_path.
|
|
227
|
+
Returns the path where the file was saved.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
cookies_path: Path to cookies JSON (Chrome export). Defaults to REVOLUTIONNEXT_COOKIES_PATH or current dir.
|
|
231
|
+
output_path: Where to save the CSV. Defaults to current dir / Parts_By_Bin_Location.csv.
|
|
232
|
+
base_url: Revolution Next base URL. Defaults to env.
|
|
233
|
+
"""
|
|
234
|
+
base_url = base_url or get_revnext_base_url_from_env()
|
|
235
|
+
default_cookies = Path.cwd() / "revnext-cookies.json"
|
|
236
|
+
cookies_path = Path(cookies_path) if cookies_path is not None else default_cookies
|
|
237
|
+
output_path = Path(output_path) if output_path is not None else (Path.cwd() / "Parts_By_Bin_Location.csv")
|
|
238
|
+
session = create_session(cookies_path, SERVICE_OBJECT, base_url)
|
|
239
|
+
return run_report_flow(
|
|
240
|
+
session,
|
|
241
|
+
SERVICE_OBJECT,
|
|
242
|
+
ACTIVITY_TAB_ID,
|
|
243
|
+
_build_submit_body,
|
|
244
|
+
output_path,
|
|
245
|
+
base_url,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
download_parts_by_bin_report()
|
|
@@ -0,0 +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()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revnext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Revolution Next (*.revolutionnext.com.au) report downloads via REST API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: revnext,revolution-next,reports,api
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
|
|
17
|
+
# revnext
|
|
18
|
+
|
|
19
|
+
Download Revolution Next (*.revolutionnext.com.au) reports (Parts Price List, Parts by Bin Location) via REST API using cookies/session.
|
|
20
|
+
|
|
21
|
+
Install: `pip install revnext`
|
|
22
|
+
Or from repo root: `pip install -e ./packages/revnext`
|
|
23
|
+
|
|
24
|
+
See the [main repo README](../README.md) for configuration and usage.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
revnext/__init__.py
|
|
4
|
+
revnext/common.py
|
|
5
|
+
revnext/config.py
|
|
6
|
+
revnext/download_all_reports.py
|
|
7
|
+
revnext/parts_by_bin_report.py
|
|
8
|
+
revnext/parts_price_list_report.py
|
|
9
|
+
revnext.egg-info/PKG-INFO
|
|
10
|
+
revnext.egg-info/SOURCES.txt
|
|
11
|
+
revnext.egg-info/dependency_links.txt
|
|
12
|
+
revnext.egg-info/requires.txt
|
|
13
|
+
revnext.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
revnext
|
revnext-0.1.0/setup.cfg
ADDED