pyprocore 1.0.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.
- pyprocore-1.0.0/PKG-INFO +241 -0
- pyprocore-1.0.0/README.md +221 -0
- pyprocore-1.0.0/app.py +176 -0
- pyprocore-1.0.0/auth/__init__.py +45 -0
- pyprocore-1.0.0/auth/oauth.py +166 -0
- pyprocore-1.0.0/auth/token_manager.py +106 -0
- pyprocore-1.0.0/auth/token_store.py +158 -0
- pyprocore-1.0.0/core/__init__.py +57 -0
- pyprocore-1.0.0/core/client.py +425 -0
- pyprocore-1.0.0/core/config.py +97 -0
- pyprocore-1.0.0/core/endpoints.py +54 -0
- pyprocore-1.0.0/core/exceptions.py +58 -0
- pyprocore-1.0.0/core/logger.py +141 -0
- pyprocore-1.0.0/models/__init__.py +25 -0
- pyprocore-1.0.0/models/base.py +11 -0
- pyprocore-1.0.0/models/resources.py +86 -0
- pyprocore-1.0.0/parser/__init__.py +17 -0
- pyprocore-1.0.0/parser/email_parser.py +165 -0
- pyprocore-1.0.0/pyprocore.egg-info/PKG-INFO +241 -0
- pyprocore-1.0.0/pyprocore.egg-info/SOURCES.txt +35 -0
- pyprocore-1.0.0/pyprocore.egg-info/dependency_links.txt +1 -0
- pyprocore-1.0.0/pyprocore.egg-info/entry_points.txt +2 -0
- pyprocore-1.0.0/pyprocore.egg-info/requires.txt +12 -0
- pyprocore-1.0.0/pyprocore.egg-info/top_level.txt +6 -0
- pyprocore-1.0.0/pyproject.toml +62 -0
- pyprocore-1.0.0/services/__init__.py +31 -0
- pyprocore-1.0.0/services/companies.py +31 -0
- pyprocore-1.0.0/services/files.py +281 -0
- pyprocore-1.0.0/services/projects.py +87 -0
- pyprocore-1.0.0/services/rfis.py +147 -0
- pyprocore-1.0.0/services/submittals.py +140 -0
- pyprocore-1.0.0/setup.cfg +4 -0
- pyprocore-1.0.0/tests/test_client.py +250 -0
- pyprocore-1.0.0/tests/test_endpoints.py +45 -0
- pyprocore-1.0.0/tests/test_files.py +227 -0
- pyprocore-1.0.0/tests/test_services.py +260 -0
- pyprocore-1.0.0/tests/test_token_management.py +217 -0
pyprocore-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyprocore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Production-ready Python SDK foundation for the Procore REST API.
|
|
5
|
+
Author-email: Author Placeholder <author@example.com>
|
|
6
|
+
License: Proprietary - placeholder
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.32.3
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
11
|
+
Requires-Dist: pydantic>=2.8.2
|
|
12
|
+
Requires-Dist: tenacity>=8.5.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: black>=24.10.0; extra == "dev"
|
|
15
|
+
Requires-Dist: coverage>=7.6.0; extra == "dev"
|
|
16
|
+
Requires-Dist: flake8>=7.1.0; extra == "dev"
|
|
17
|
+
Requires-Dist: isort>=5.13.2; extra == "dev"
|
|
18
|
+
Requires-Dist: mypy>=1.13.0; extra == "dev"
|
|
19
|
+
Requires-Dist: types-requests>=2.32.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# Procore SDK
|
|
22
|
+
|
|
23
|
+
Production-quality Python SDK and automation foundation for the Procore REST API.
|
|
24
|
+
|
|
25
|
+
The SDK handles configuration, OAuth token refresh, authenticated HTTP requests,
|
|
26
|
+
pagination, typed response models, structured logging, and attachment downloads
|
|
27
|
+
for companies, projects, RFIs, and submittals.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Requires Python 3.12+.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
.venv/bin/python -m pip install --upgrade pip
|
|
36
|
+
.venv/bin/python -m pip install -r requirements.txt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
Copy the example file and fill in real values:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cp .env.example .env
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Required variables:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
PROCORE_CLIENT_ID=your_client_id
|
|
51
|
+
PROCORE_CLIENT_SECRET=your_client_secret
|
|
52
|
+
PROCORE_REDIRECT_URI=http://localhost:8080/callback
|
|
53
|
+
PROCORE_LOGIN_URL=https://login.procore.com
|
|
54
|
+
PROCORE_API_BASE=https://api.procore.com
|
|
55
|
+
PROCORE_COMPANY_ID=123456
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Secrets, tokens, URLs, and company IDs are never hardcoded in source.
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
Exchange the first authorization code and save the token locally:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from auth.oauth import exchange_authorization_code
|
|
66
|
+
from auth.token_manager import TokenManager
|
|
67
|
+
|
|
68
|
+
token_response = exchange_authorization_code("authorization-code-from-procore")
|
|
69
|
+
TokenManager().save_oauth_response(token_response)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
After that, SDK clients call:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from auth.token_manager import get_access_token
|
|
76
|
+
|
|
77
|
+
access_token = get_access_token()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Expired access tokens refresh automatically when a refresh token is available.
|
|
81
|
+
|
|
82
|
+
## CLI Examples
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
.venv/bin/python app.py companies
|
|
86
|
+
.venv/bin/python app.py projects
|
|
87
|
+
.venv/bin/python app.py rfis --project 352338
|
|
88
|
+
.venv/bin/python app.py rfi --project 352338 --id 102784
|
|
89
|
+
.venv/bin/python app.py submittals --project 352338
|
|
90
|
+
.venv/bin/python app.py submittal --project 352338 --id 309641
|
|
91
|
+
.venv/bin/python app.py download-rfi --project 352338 --id 102784
|
|
92
|
+
.venv/bin/python app.py download-submittal --project 352338 --id 309641
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The CLI prints nicely formatted JSON. Typed SDK models are serialized with
|
|
96
|
+
`model_dump(mode="json")`.
|
|
97
|
+
|
|
98
|
+
## SDK Examples
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from services import (
|
|
102
|
+
download_rfi_attachments,
|
|
103
|
+
download_submittal_attachments,
|
|
104
|
+
get_rfi,
|
|
105
|
+
get_submittal,
|
|
106
|
+
list_companies,
|
|
107
|
+
list_projects,
|
|
108
|
+
list_rfis,
|
|
109
|
+
list_submittals,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
companies = list_companies()
|
|
113
|
+
projects = list_projects(company_id=123456)
|
|
114
|
+
|
|
115
|
+
rfis = list_rfis(project_id=352338)
|
|
116
|
+
rfi = get_rfi(project_id=352338, rfi_id=102784)
|
|
117
|
+
first_attachment_url = rfi.questions[0].attachments[0].url
|
|
118
|
+
|
|
119
|
+
submittals = list_submittals(project_id=352338)
|
|
120
|
+
submittal = get_submittal(project_id=352338, submittal_id=309641)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
All typed models can be serialized back to JSON:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
json_payload = rfi.model_dump(mode="json")
|
|
127
|
+
json_string = rfi.model_dump_json()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Downloading Attachments
|
|
131
|
+
|
|
132
|
+
RFI attachments are read from:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
questions[].attachments[].url
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Submittal attachments are read from:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
attachments[].url
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Download from services:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
rfi_files = download_rfi_attachments(project_id=352338, rfi_id=102784)
|
|
148
|
+
submittal_files = download_submittal_attachments(
|
|
149
|
+
project_id=352338,
|
|
150
|
+
submittal_id=309641,
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The shared file service supports safe filenames, streaming writes, retries,
|
|
155
|
+
progress logging, batch downloads, and skip-existing behavior by default.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from services.files import FileDownloadService
|
|
159
|
+
|
|
160
|
+
files = FileDownloadService().download_attachments(
|
|
161
|
+
attachments,
|
|
162
|
+
"downloads/custom",
|
|
163
|
+
fallback_prefix="attachment",
|
|
164
|
+
overwrite=False,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Pagination
|
|
169
|
+
|
|
170
|
+
Collection service methods use `ProcoreClient.get_all()`, which follows Procore
|
|
171
|
+
pagination headers automatically. Business logic should call the service method
|
|
172
|
+
or `get_all()` and should not manually request page 2.
|
|
173
|
+
|
|
174
|
+
## Logging
|
|
175
|
+
|
|
176
|
+
The SDK writes structured logs to:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
logs/sdk.log
|
|
180
|
+
logs/errors.log
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
API request logs include method, endpoint, response status, elapsed time, and
|
|
184
|
+
retry count. Exception logs include stack traces, exception type, request URL,
|
|
185
|
+
HTTP status, and response body when available.
|
|
186
|
+
|
|
187
|
+
The logger redacts sensitive keys such as authorization headers, access tokens,
|
|
188
|
+
refresh tokens, and client secrets.
|
|
189
|
+
|
|
190
|
+
## Architecture
|
|
191
|
+
|
|
192
|
+
- `auth/`: OAuth exchange, token persistence, token refresh
|
|
193
|
+
- `core/`: configuration, endpoint paths, HTTP client, logging, exceptions
|
|
194
|
+
- `models/`: Pydantic response models
|
|
195
|
+
- `services/`: company, project, RFI, submittal, and file services
|
|
196
|
+
- `parser/`: email parsing utilities for future automation
|
|
197
|
+
- `tests/`: mocked unit tests with no live Procore dependency
|
|
198
|
+
|
|
199
|
+
## Verified Endpoint Assumptions
|
|
200
|
+
|
|
201
|
+
- `GET /rest/v1.0/companies`
|
|
202
|
+
- `GET /rest/v1.0/companies/{company_id}/projects`
|
|
203
|
+
- `GET /rest/v1.1/projects/{project_id}/rfis`
|
|
204
|
+
- `GET /rest/v1.1/projects/{project_id}/rfis/{rfi_id}`
|
|
205
|
+
- `GET /rest/v1.1/projects/{project_id}/submittals`
|
|
206
|
+
- `GET /rest/v1.1/projects/{project_id}/submittals/{submittal_id}`
|
|
207
|
+
|
|
208
|
+
## Troubleshooting
|
|
209
|
+
|
|
210
|
+
`ConfigurationError`
|
|
211
|
+
: Check that `.env` exists and all required keys are present.
|
|
212
|
+
|
|
213
|
+
`AuthenticationError`
|
|
214
|
+
: Complete the first OAuth code exchange and confirm `auth/token_store.json`
|
|
215
|
+
contains a refresh token.
|
|
216
|
+
|
|
217
|
+
`AuthorizationError`
|
|
218
|
+
: Confirm the Procore user has access to the target company/project/resource.
|
|
219
|
+
|
|
220
|
+
`ResourceNotFoundError`
|
|
221
|
+
: Confirm project, RFI, or submittal IDs are correct for the configured company.
|
|
222
|
+
|
|
223
|
+
Attachment files are not downloading
|
|
224
|
+
: Check `logs/errors.log` for HTTP status and response body details. Existing
|
|
225
|
+
files are skipped unless `overwrite=True`.
|
|
226
|
+
|
|
227
|
+
## Tests
|
|
228
|
+
|
|
229
|
+
Run unit tests:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
.venv/bin/python -m unittest discover -s tests
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Run coverage:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
.venv/bin/python -m coverage run -m unittest discover -s tests
|
|
239
|
+
.venv/bin/python -m coverage report
|
|
240
|
+
```
|
|
241
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Procore SDK
|
|
2
|
+
|
|
3
|
+
Production-quality Python SDK and automation foundation for the Procore REST API.
|
|
4
|
+
|
|
5
|
+
The SDK handles configuration, OAuth token refresh, authenticated HTTP requests,
|
|
6
|
+
pagination, typed response models, structured logging, and attachment downloads
|
|
7
|
+
for companies, projects, RFIs, and submittals.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Requires Python 3.12+.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python3 -m venv .venv
|
|
15
|
+
.venv/bin/python -m pip install --upgrade pip
|
|
16
|
+
.venv/bin/python -m pip install -r requirements.txt
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
Copy the example file and fill in real values:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cp .env.example .env
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Required variables:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
PROCORE_CLIENT_ID=your_client_id
|
|
31
|
+
PROCORE_CLIENT_SECRET=your_client_secret
|
|
32
|
+
PROCORE_REDIRECT_URI=http://localhost:8080/callback
|
|
33
|
+
PROCORE_LOGIN_URL=https://login.procore.com
|
|
34
|
+
PROCORE_API_BASE=https://api.procore.com
|
|
35
|
+
PROCORE_COMPANY_ID=123456
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Secrets, tokens, URLs, and company IDs are never hardcoded in source.
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
Exchange the first authorization code and save the token locally:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from auth.oauth import exchange_authorization_code
|
|
46
|
+
from auth.token_manager import TokenManager
|
|
47
|
+
|
|
48
|
+
token_response = exchange_authorization_code("authorization-code-from-procore")
|
|
49
|
+
TokenManager().save_oauth_response(token_response)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After that, SDK clients call:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from auth.token_manager import get_access_token
|
|
56
|
+
|
|
57
|
+
access_token = get_access_token()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Expired access tokens refresh automatically when a refresh token is available.
|
|
61
|
+
|
|
62
|
+
## CLI Examples
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
.venv/bin/python app.py companies
|
|
66
|
+
.venv/bin/python app.py projects
|
|
67
|
+
.venv/bin/python app.py rfis --project 352338
|
|
68
|
+
.venv/bin/python app.py rfi --project 352338 --id 102784
|
|
69
|
+
.venv/bin/python app.py submittals --project 352338
|
|
70
|
+
.venv/bin/python app.py submittal --project 352338 --id 309641
|
|
71
|
+
.venv/bin/python app.py download-rfi --project 352338 --id 102784
|
|
72
|
+
.venv/bin/python app.py download-submittal --project 352338 --id 309641
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The CLI prints nicely formatted JSON. Typed SDK models are serialized with
|
|
76
|
+
`model_dump(mode="json")`.
|
|
77
|
+
|
|
78
|
+
## SDK Examples
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from services import (
|
|
82
|
+
download_rfi_attachments,
|
|
83
|
+
download_submittal_attachments,
|
|
84
|
+
get_rfi,
|
|
85
|
+
get_submittal,
|
|
86
|
+
list_companies,
|
|
87
|
+
list_projects,
|
|
88
|
+
list_rfis,
|
|
89
|
+
list_submittals,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
companies = list_companies()
|
|
93
|
+
projects = list_projects(company_id=123456)
|
|
94
|
+
|
|
95
|
+
rfis = list_rfis(project_id=352338)
|
|
96
|
+
rfi = get_rfi(project_id=352338, rfi_id=102784)
|
|
97
|
+
first_attachment_url = rfi.questions[0].attachments[0].url
|
|
98
|
+
|
|
99
|
+
submittals = list_submittals(project_id=352338)
|
|
100
|
+
submittal = get_submittal(project_id=352338, submittal_id=309641)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
All typed models can be serialized back to JSON:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
json_payload = rfi.model_dump(mode="json")
|
|
107
|
+
json_string = rfi.model_dump_json()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Downloading Attachments
|
|
111
|
+
|
|
112
|
+
RFI attachments are read from:
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
questions[].attachments[].url
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Submittal attachments are read from:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
attachments[].url
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Download from services:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
rfi_files = download_rfi_attachments(project_id=352338, rfi_id=102784)
|
|
128
|
+
submittal_files = download_submittal_attachments(
|
|
129
|
+
project_id=352338,
|
|
130
|
+
submittal_id=309641,
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The shared file service supports safe filenames, streaming writes, retries,
|
|
135
|
+
progress logging, batch downloads, and skip-existing behavior by default.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from services.files import FileDownloadService
|
|
139
|
+
|
|
140
|
+
files = FileDownloadService().download_attachments(
|
|
141
|
+
attachments,
|
|
142
|
+
"downloads/custom",
|
|
143
|
+
fallback_prefix="attachment",
|
|
144
|
+
overwrite=False,
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Pagination
|
|
149
|
+
|
|
150
|
+
Collection service methods use `ProcoreClient.get_all()`, which follows Procore
|
|
151
|
+
pagination headers automatically. Business logic should call the service method
|
|
152
|
+
or `get_all()` and should not manually request page 2.
|
|
153
|
+
|
|
154
|
+
## Logging
|
|
155
|
+
|
|
156
|
+
The SDK writes structured logs to:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
logs/sdk.log
|
|
160
|
+
logs/errors.log
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
API request logs include method, endpoint, response status, elapsed time, and
|
|
164
|
+
retry count. Exception logs include stack traces, exception type, request URL,
|
|
165
|
+
HTTP status, and response body when available.
|
|
166
|
+
|
|
167
|
+
The logger redacts sensitive keys such as authorization headers, access tokens,
|
|
168
|
+
refresh tokens, and client secrets.
|
|
169
|
+
|
|
170
|
+
## Architecture
|
|
171
|
+
|
|
172
|
+
- `auth/`: OAuth exchange, token persistence, token refresh
|
|
173
|
+
- `core/`: configuration, endpoint paths, HTTP client, logging, exceptions
|
|
174
|
+
- `models/`: Pydantic response models
|
|
175
|
+
- `services/`: company, project, RFI, submittal, and file services
|
|
176
|
+
- `parser/`: email parsing utilities for future automation
|
|
177
|
+
- `tests/`: mocked unit tests with no live Procore dependency
|
|
178
|
+
|
|
179
|
+
## Verified Endpoint Assumptions
|
|
180
|
+
|
|
181
|
+
- `GET /rest/v1.0/companies`
|
|
182
|
+
- `GET /rest/v1.0/companies/{company_id}/projects`
|
|
183
|
+
- `GET /rest/v1.1/projects/{project_id}/rfis`
|
|
184
|
+
- `GET /rest/v1.1/projects/{project_id}/rfis/{rfi_id}`
|
|
185
|
+
- `GET /rest/v1.1/projects/{project_id}/submittals`
|
|
186
|
+
- `GET /rest/v1.1/projects/{project_id}/submittals/{submittal_id}`
|
|
187
|
+
|
|
188
|
+
## Troubleshooting
|
|
189
|
+
|
|
190
|
+
`ConfigurationError`
|
|
191
|
+
: Check that `.env` exists and all required keys are present.
|
|
192
|
+
|
|
193
|
+
`AuthenticationError`
|
|
194
|
+
: Complete the first OAuth code exchange and confirm `auth/token_store.json`
|
|
195
|
+
contains a refresh token.
|
|
196
|
+
|
|
197
|
+
`AuthorizationError`
|
|
198
|
+
: Confirm the Procore user has access to the target company/project/resource.
|
|
199
|
+
|
|
200
|
+
`ResourceNotFoundError`
|
|
201
|
+
: Confirm project, RFI, or submittal IDs are correct for the configured company.
|
|
202
|
+
|
|
203
|
+
Attachment files are not downloading
|
|
204
|
+
: Check `logs/errors.log` for HTTP status and response body details. Existing
|
|
205
|
+
files are skipped unless `overwrite=True`.
|
|
206
|
+
|
|
207
|
+
## Tests
|
|
208
|
+
|
|
209
|
+
Run unit tests:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
.venv/bin/python -m unittest discover -s tests
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Run coverage:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
.venv/bin/python -m coverage run -m unittest discover -s tests
|
|
219
|
+
.venv/bin/python -m coverage report
|
|
220
|
+
```
|
|
221
|
+
|
pyprocore-1.0.0/app.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Command-line entrypoint for Procore SDK operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from core.config import get_settings
|
|
13
|
+
from services import (
|
|
14
|
+
download_rfi_attachments,
|
|
15
|
+
download_submittal_attachments,
|
|
16
|
+
get_rfi,
|
|
17
|
+
get_submittal,
|
|
18
|
+
list_companies,
|
|
19
|
+
list_projects,
|
|
20
|
+
list_rfis,
|
|
21
|
+
list_submittals,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
"""Build the command-line parser."""
|
|
27
|
+
parser = argparse.ArgumentParser(description="Procore SDK utility commands")
|
|
28
|
+
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
subcommands.add_parser("companies", help="List companies")
|
|
31
|
+
|
|
32
|
+
projects_parser = subcommands.add_parser("projects", help="List projects")
|
|
33
|
+
projects_parser.add_argument("--company-id", type=int, default=None)
|
|
34
|
+
|
|
35
|
+
rfis_parser = subcommands.add_parser("rfis", help="List RFIs for a project")
|
|
36
|
+
rfis_parser.add_argument(
|
|
37
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
rfi_parser = subcommands.add_parser("rfi", help="Get one RFI")
|
|
41
|
+
rfi_parser.add_argument(
|
|
42
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
43
|
+
)
|
|
44
|
+
rfi_parser.add_argument("--id", "--rfi-id", dest="rfi_id", type=int, required=True)
|
|
45
|
+
|
|
46
|
+
rfi_download_parser = _add_alias_parser(
|
|
47
|
+
subcommands,
|
|
48
|
+
"download-rfi",
|
|
49
|
+
["download-rfi-attachments"],
|
|
50
|
+
"download-rfi-attachments",
|
|
51
|
+
help="Download RFI attachments",
|
|
52
|
+
)
|
|
53
|
+
rfi_download_parser.add_argument(
|
|
54
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
55
|
+
)
|
|
56
|
+
rfi_download_parser.add_argument(
|
|
57
|
+
"--id", "--rfi-id", dest="rfi_id", type=int, required=True
|
|
58
|
+
)
|
|
59
|
+
rfi_download_parser.add_argument("--destination-dir", type=Path, default=None)
|
|
60
|
+
|
|
61
|
+
submittals_parser = subcommands.add_parser(
|
|
62
|
+
"submittals",
|
|
63
|
+
help="List submittals for a project",
|
|
64
|
+
)
|
|
65
|
+
submittals_parser.add_argument(
|
|
66
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
submittal_parser = subcommands.add_parser("submittal", help="Get one submittal")
|
|
70
|
+
submittal_parser.add_argument(
|
|
71
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
72
|
+
)
|
|
73
|
+
submittal_parser.add_argument(
|
|
74
|
+
"--id", "--submittal-id", dest="submittal_id", type=int, required=True
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
submittal_download_parser = _add_alias_parser(
|
|
78
|
+
subcommands,
|
|
79
|
+
"download-submittal",
|
|
80
|
+
["download-submittal-attachments"],
|
|
81
|
+
"download-submittal-attachments",
|
|
82
|
+
help="Download submittal attachments",
|
|
83
|
+
)
|
|
84
|
+
submittal_download_parser.add_argument(
|
|
85
|
+
"--project", "--project-id", dest="project_id", type=int, required=True
|
|
86
|
+
)
|
|
87
|
+
submittal_download_parser.add_argument(
|
|
88
|
+
"--id", "--submittal-id", dest="submittal_id", type=int, required=True
|
|
89
|
+
)
|
|
90
|
+
submittal_download_parser.add_argument("--destination-dir", type=Path, default=None)
|
|
91
|
+
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _add_alias_parser(
|
|
96
|
+
subcommands: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
97
|
+
name: str,
|
|
98
|
+
aliases: list[str],
|
|
99
|
+
legacy_name: str,
|
|
100
|
+
**kwargs: Any,
|
|
101
|
+
) -> argparse.ArgumentParser:
|
|
102
|
+
"""Add a subcommand with aliases when supported by argparse."""
|
|
103
|
+
parser = subcommands.add_parser(name, aliases=aliases, **kwargs)
|
|
104
|
+
parser.set_defaults(command=name, legacy_command=legacy_name)
|
|
105
|
+
return parser
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_command(args: argparse.Namespace) -> Any:
|
|
109
|
+
"""Run a parsed CLI command and return serializable output."""
|
|
110
|
+
if args.command == "companies":
|
|
111
|
+
return list_companies()
|
|
112
|
+
|
|
113
|
+
if args.command == "projects":
|
|
114
|
+
company_id = args.company_id or get_settings().company_id
|
|
115
|
+
return list_projects(company_id)
|
|
116
|
+
|
|
117
|
+
if args.command == "rfis":
|
|
118
|
+
return list_rfis(args.project_id)
|
|
119
|
+
|
|
120
|
+
if args.command == "rfi":
|
|
121
|
+
return get_rfi(args.project_id, args.rfi_id)
|
|
122
|
+
|
|
123
|
+
if args.command == "download-rfi":
|
|
124
|
+
return [
|
|
125
|
+
str(path)
|
|
126
|
+
for path in download_rfi_attachments(
|
|
127
|
+
args.project_id,
|
|
128
|
+
args.rfi_id,
|
|
129
|
+
args.destination_dir,
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if args.command == "submittals":
|
|
134
|
+
return list_submittals(args.project_id)
|
|
135
|
+
|
|
136
|
+
if args.command == "submittal":
|
|
137
|
+
return get_submittal(args.project_id, args.submittal_id)
|
|
138
|
+
|
|
139
|
+
if args.command == "download-submittal":
|
|
140
|
+
return [
|
|
141
|
+
str(path)
|
|
142
|
+
for path in download_submittal_attachments(
|
|
143
|
+
args.project_id,
|
|
144
|
+
args.submittal_id,
|
|
145
|
+
args.destination_dir,
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
raise ValueError(f"Unsupported command: {args.command}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def to_serializable(value: Any) -> Any:
|
|
153
|
+
"""Convert SDK output into JSON-serializable data."""
|
|
154
|
+
if isinstance(value, BaseModel):
|
|
155
|
+
return value.model_dump(mode="json")
|
|
156
|
+
if isinstance(value, list):
|
|
157
|
+
return [to_serializable(item) for item in value]
|
|
158
|
+
if isinstance(value, tuple):
|
|
159
|
+
return [to_serializable(item) for item in value]
|
|
160
|
+
if isinstance(value, dict):
|
|
161
|
+
return {key: to_serializable(item) for key, item in value.items()}
|
|
162
|
+
if isinstance(value, Path):
|
|
163
|
+
return str(value)
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main() -> None:
|
|
168
|
+
"""Run the CLI entrypoint."""
|
|
169
|
+
parser = build_parser()
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
result = run_command(args)
|
|
172
|
+
print(json.dumps(to_serializable(result), indent=2, default=str))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Authentication utilities for the Procore SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"OAuthClient",
|
|
9
|
+
"OAuthTokenResponse",
|
|
10
|
+
"StoredToken",
|
|
11
|
+
"TokenManager",
|
|
12
|
+
"TokenStore",
|
|
13
|
+
"get_access_token",
|
|
14
|
+
"load_token",
|
|
15
|
+
"save_token",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(name: str) -> Any:
|
|
20
|
+
"""Lazily expose auth objects without creating import cycles."""
|
|
21
|
+
if name in {"OAuthClient", "OAuthTokenResponse"}:
|
|
22
|
+
from auth.oauth import OAuthClient, OAuthTokenResponse
|
|
23
|
+
|
|
24
|
+
return {"OAuthClient": OAuthClient, "OAuthTokenResponse": OAuthTokenResponse}[
|
|
25
|
+
name
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
if name in {"TokenManager", "get_access_token"}:
|
|
29
|
+
from auth.token_manager import TokenManager, get_access_token
|
|
30
|
+
|
|
31
|
+
return {"TokenManager": TokenManager, "get_access_token": get_access_token}[
|
|
32
|
+
name
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
if name in {"StoredToken", "TokenStore", "load_token", "save_token"}:
|
|
36
|
+
from auth.token_store import StoredToken, TokenStore, load_token, save_token
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"StoredToken": StoredToken,
|
|
40
|
+
"TokenStore": TokenStore,
|
|
41
|
+
"load_token": load_token,
|
|
42
|
+
"save_token": save_token,
|
|
43
|
+
}[name]
|
|
44
|
+
|
|
45
|
+
raise AttributeError(f"module 'auth' has no attribute {name!r}")
|