pyprocore 1.0.0__tar.gz → 1.0.2__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.2/LICENSE +3 -0
- pyprocore-1.0.2/PKG-INFO +371 -0
- pyprocore-1.0.2/README.md +335 -0
- pyprocore-1.0.2/pyprocore/__init__.py +3 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/app.py +4 -8
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/auth/__init__.py +5 -9
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/auth/oauth.py +5 -11
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/auth/token_manager.py +3 -3
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/auth/token_store.py +8 -5
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/__init__.py +4 -4
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/client.py +5 -7
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/config.py +1 -1
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/logger.py +3 -9
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/models/__init__.py +3 -3
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/models/resources.py +1 -1
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/parser/__init__.py +1 -1
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/parser/email_parser.py +4 -9
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/__init__.py +5 -5
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/companies.py +4 -6
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/files.py +9 -19
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/projects.py +6 -8
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/rfis.py +7 -11
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/services/submittals.py +10 -22
- pyprocore-1.0.2/pyprocore.egg-info/PKG-INFO +371 -0
- pyprocore-1.0.2/pyprocore.egg-info/SOURCES.txt +39 -0
- pyprocore-1.0.2/pyprocore.egg-info/entry_points.txt +2 -0
- pyprocore-1.0.2/pyprocore.egg-info/top_level.txt +1 -0
- pyprocore-1.0.2/pyproject.toml +86 -0
- pyprocore-1.0.2/tests/test_app.py +192 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2}/tests/test_client.py +6 -12
- {pyprocore-1.0.0 → pyprocore-1.0.2}/tests/test_endpoints.py +1 -1
- {pyprocore-1.0.0 → pyprocore-1.0.2}/tests/test_files.py +7 -17
- {pyprocore-1.0.0 → pyprocore-1.0.2}/tests/test_services.py +17 -25
- pyprocore-1.0.2/tests/test_support_modules.py +336 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2}/tests/test_token_management.py +8 -16
- pyprocore-1.0.0/PKG-INFO +0 -241
- pyprocore-1.0.0/README.md +0 -221
- pyprocore-1.0.0/pyprocore.egg-info/PKG-INFO +0 -241
- pyprocore-1.0.0/pyprocore.egg-info/SOURCES.txt +0 -35
- pyprocore-1.0.0/pyprocore.egg-info/entry_points.txt +0 -2
- pyprocore-1.0.0/pyprocore.egg-info/top_level.txt +0 -6
- pyprocore-1.0.0/pyproject.toml +0 -62
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/endpoints.py +0 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/core/exceptions.py +0 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2/pyprocore}/models/base.py +0 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2}/pyprocore.egg-info/dependency_links.txt +0 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2}/pyprocore.egg-info/requires.txt +0 -0
- {pyprocore-1.0.0 → pyprocore-1.0.2}/setup.cfg +0 -0
pyprocore-1.0.2/LICENSE
ADDED
pyprocore-1.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyprocore
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: A production-ready Python SDK for the Procore REST API.
|
|
5
|
+
Author-email: Vibhanshu Mishra <vibhanshumishra72@gmail.com>
|
|
6
|
+
License: License placeholder.
|
|
7
|
+
|
|
8
|
+
Replace this file with the approved internal or open-source license before publishing.
|
|
9
|
+
|
|
10
|
+
Project-URL: Homepage, https://github.com/vibhanshu-mishra/pyprocore
|
|
11
|
+
Project-URL: Repository, https://github.com/vibhanshu-mishra/pyprocore
|
|
12
|
+
Project-URL: Issues, https://github.com/vibhanshu-mishra/pyprocore/issues
|
|
13
|
+
Project-URL: Documentation, https://github.com/vibhanshu-mishra/pyprocore#readme
|
|
14
|
+
Keywords: procore,construction,sdk,python,automation,oauth,api,rfi,submittals
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.32.3
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
26
|
+
Requires-Dist: pydantic>=2.8.2
|
|
27
|
+
Requires-Dist: tenacity>=8.5.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: black>=24.10.0; extra == "dev"
|
|
30
|
+
Requires-Dist: coverage>=7.6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: flake8>=7.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: isort>=5.13.2; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.13.0; extra == "dev"
|
|
34
|
+
Requires-Dist: types-requests>=2.32.0; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# PyProcore
|
|
38
|
+
|
|
39
|
+
> A production-ready Python SDK for building automation, integrations, and AI workflows on top of the Procore REST API.
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/pyprocore/)
|
|
42
|
+
[](https://pypi.org/project/pyprocore/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://github.com/vibhanshu-mishra/pyprocore/actions/workflows/tests.yml)
|
|
45
|
+
[](#tests)
|
|
46
|
+
|
|
47
|
+
PyProcore handles the parts of a Procore integration that are tedious and easy to get wrong — OAuth, token refresh, pagination, retries, typed responses, structured logging, and attachment downloads — so you work with Python objects instead of raw JSON and API plumbing.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Why PyProcore
|
|
52
|
+
|
|
53
|
+
Calling the Procore REST API directly means managing the OAuth handshake, refreshing expired tokens, following pagination headers, retrying failed requests, and parsing untyped JSON on every call.
|
|
54
|
+
|
|
55
|
+
PyProcore does that once, correctly, behind a clean interface. You call a service method and get back a typed Pydantic object. It is designed as the foundation layer for higher-level tools built on Procore data: engineering assistants, document analysis, workflow automation, and AI-powered review.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
**Authentication and transport**
|
|
62
|
+
|
|
63
|
+
- OAuth 2.0 authorization-code flow
|
|
64
|
+
- Automatic access-token refresh
|
|
65
|
+
- Automatic pagination via Procore response headers
|
|
66
|
+
- Request retries and structured logging with secret redaction
|
|
67
|
+
|
|
68
|
+
**API coverage (v1.0)**
|
|
69
|
+
|
|
70
|
+
- Companies
|
|
71
|
+
- Projects
|
|
72
|
+
- RFIs
|
|
73
|
+
- Submittals
|
|
74
|
+
- Attachment downloads
|
|
75
|
+
|
|
76
|
+
**Developer experience**
|
|
77
|
+
|
|
78
|
+
- Typed Pydantic response models
|
|
79
|
+
- Command-line interface
|
|
80
|
+
- 73 unit tests at 94% coverage, mocked with no live Procore dependency
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
| Package | Responsibility |
|
|
87
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
88
|
+
| `pyprocore/auth/` | OAuth exchange, token persistence, token refresh |
|
|
89
|
+
| `pyprocore/core/` | Configuration, endpoint paths, HTTP client, logging, exceptions |
|
|
90
|
+
| `pyprocore/models/` | Pydantic response models |
|
|
91
|
+
| `pyprocore/services/` | Company, project, RFI, submittal, and file services |
|
|
92
|
+
| `pyprocore/parser/` | Email parsing utilities for future automation |
|
|
93
|
+
| `tests/` | Mocked unit tests with no live Procore dependency |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
Requires Python 3.12+.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip3 install pyprocore
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For local development:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/vibhanshu-mishra/pyprocore.git
|
|
109
|
+
cd pyprocore
|
|
110
|
+
python3 -m venv .venv
|
|
111
|
+
.venv/bin/python -m pip install --upgrade pip
|
|
112
|
+
.venv/bin/python -m pip install -e .
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Quick Example
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from pyprocore.services import list_projects
|
|
121
|
+
|
|
122
|
+
projects = list_projects(company_id=123456)
|
|
123
|
+
|
|
124
|
+
for project in projects:
|
|
125
|
+
print(project.name)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
Copy the example file and fill in real values:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
cp .env.example .env
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Required variables:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
PROCORE_CLIENT_ID=your_client_id
|
|
142
|
+
PROCORE_CLIENT_SECRET=your_client_secret
|
|
143
|
+
PROCORE_REDIRECT_URI=http://localhost:8080/callback
|
|
144
|
+
PROCORE_LOGIN_URL=https://login.procore.com
|
|
145
|
+
PROCORE_API_BASE=https://api.procore.com
|
|
146
|
+
PROCORE_COMPANY_ID=123456
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Secrets, tokens, URLs, and company IDs are never hardcoded in source.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Authentication
|
|
154
|
+
|
|
155
|
+
Exchange the first authorization code and save the token locally:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from pyprocore.auth.oauth import exchange_authorization_code
|
|
159
|
+
from pyprocore.auth.token_manager import TokenManager
|
|
160
|
+
|
|
161
|
+
token_response = exchange_authorization_code("authorization-code-from-procore")
|
|
162
|
+
TokenManager().save_oauth_response(token_response)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
After that, SDK clients read the token automatically:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from pyprocore.auth.token_manager import get_access_token
|
|
169
|
+
|
|
170
|
+
access_token = get_access_token()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Expired access tokens refresh automatically whenever a refresh token is available.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Usage
|
|
178
|
+
|
|
179
|
+
Once `.env` is configured and the one-time OAuth exchange is complete, calls return typed objects:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from pyprocore.services import list_projects
|
|
183
|
+
|
|
184
|
+
for project in list_projects(company_id=123456):
|
|
185
|
+
print(project.name)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Full service surface:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from pyprocore.services import (
|
|
192
|
+
download_rfi_attachments,
|
|
193
|
+
download_submittal_attachments,
|
|
194
|
+
get_rfi,
|
|
195
|
+
get_submittal,
|
|
196
|
+
list_companies,
|
|
197
|
+
list_projects,
|
|
198
|
+
list_rfis,
|
|
199
|
+
list_submittals,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
companies = list_companies()
|
|
203
|
+
projects = list_projects(company_id=123456)
|
|
204
|
+
|
|
205
|
+
rfis = list_rfis(project_id=352338)
|
|
206
|
+
rfi = get_rfi(project_id=352338, rfi_id=102784)
|
|
207
|
+
first_attachment_url = rfi.questions[0].attachments[0].url
|
|
208
|
+
|
|
209
|
+
submittals = list_submittals(project_id=352338)
|
|
210
|
+
submittal = get_submittal(project_id=352338, submittal_id=309641)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Every typed model serializes back to JSON:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
json_payload = rfi.model_dump(mode="json")
|
|
217
|
+
json_string = rfi.model_dump_json()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Downloading Attachments
|
|
223
|
+
|
|
224
|
+
Attachment URLs live at:
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
RFI: questions[].attachments[].url
|
|
228
|
+
Submittal: attachments[].url
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Download through the service functions:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
rfi_files = download_rfi_attachments(project_id=352338, rfi_id=102784)
|
|
235
|
+
submittal_files = download_submittal_attachments(
|
|
236
|
+
project_id=352338,
|
|
237
|
+
submittal_id=309641,
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The shared file service supports safe filenames, streaming writes, retries, progress logging, batch downloads, and skip-existing behavior by default:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
from pyprocore.services.files import FileDownloadService
|
|
245
|
+
|
|
246
|
+
files = FileDownloadService().download_attachments(
|
|
247
|
+
attachments,
|
|
248
|
+
"downloads/custom",
|
|
249
|
+
fallback_prefix="attachment",
|
|
250
|
+
overwrite=False,
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## CLI
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
procore-sdk companies
|
|
260
|
+
procore-sdk projects
|
|
261
|
+
procore-sdk rfis --project 352338
|
|
262
|
+
procore-sdk rfi --project 352338 --id 102784
|
|
263
|
+
procore-sdk submittals --project 352338
|
|
264
|
+
procore-sdk submittal --project 352338 --id 309641
|
|
265
|
+
procore-sdk download-rfi --project 352338 --id 102784
|
|
266
|
+
procore-sdk download-submittal --project 352338 --id 309641
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The CLI prints formatted JSON. Typed models are serialized with `model_dump(mode="json")`.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Pagination
|
|
274
|
+
|
|
275
|
+
Collection methods use `ProcoreClient.get_all()`, which follows Procore pagination headers automatically. Business logic should call the service method or `get_all()` directly and never request page 2 by hand.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Logging
|
|
280
|
+
|
|
281
|
+
Structured logs are written to:
|
|
282
|
+
|
|
283
|
+
```text
|
|
284
|
+
logs/sdk.log
|
|
285
|
+
logs/errors.log
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Request logs record method, endpoint, status, elapsed time, and retry count. Exception logs record stack traces, exception type, request URL, HTTP status, and response body when available. The logger redacts sensitive keys such as authorization headers, access tokens, refresh tokens, and client secrets.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Implemented Endpoints (v1.0)
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
GET /rest/v1.0/companies
|
|
296
|
+
GET /rest/v1.0/companies/{company_id}/projects
|
|
297
|
+
GET /rest/v1.1/projects/{project_id}/rfis
|
|
298
|
+
GET /rest/v1.1/projects/{project_id}/rfis/{rfi_id}
|
|
299
|
+
GET /rest/v1.1/projects/{project_id}/submittals
|
|
300
|
+
GET /rest/v1.1/projects/{project_id}/submittals/{submittal_id}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Roadmap
|
|
306
|
+
|
|
307
|
+
**Planned endpoints**
|
|
308
|
+
|
|
309
|
+
- Drawings
|
|
310
|
+
- Documents
|
|
311
|
+
- Specifications
|
|
312
|
+
- Daily Logs
|
|
313
|
+
- Photos
|
|
314
|
+
- Correspondence
|
|
315
|
+
- Observations
|
|
316
|
+
|
|
317
|
+
**Planned capabilities**
|
|
318
|
+
|
|
319
|
+
- AI workflow examples built on the SDK
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Troubleshooting
|
|
324
|
+
|
|
325
|
+
| Error | Likely cause and fix |
|
|
326
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
327
|
+
| `ConfigurationError` | `.env` is missing or a required key is absent. |
|
|
328
|
+
| `AuthenticationError` | Complete the first OAuth code exchange; confirm `pyprocore/auth/token_store.json` holds a refresh token. |
|
|
329
|
+
| `AuthorizationError` | The Procore user lacks access to the target company, project, or resource. |
|
|
330
|
+
| `ResourceNotFoundError` | Project, RFI, or submittal ID is wrong for the configured company. |
|
|
331
|
+
| Attachments not downloading | Check `logs/errors.log` for HTTP status and response body. Existing files are skipped unless `overwrite=True`. |
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Tests
|
|
336
|
+
|
|
337
|
+
Run unit tests:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
.venv/bin/python -m unittest discover -s tests
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Run coverage:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
.venv/bin/python -m coverage run -m unittest discover -s tests
|
|
347
|
+
.venv/bin/python -m coverage report
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Supported Environments
|
|
353
|
+
|
|
354
|
+
- Procore Production
|
|
355
|
+
- Procore Sandbox
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Contributing
|
|
360
|
+
|
|
361
|
+
Contributions, issues, and feature requests are welcome. Please open an issue before submitting large changes.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
Released under the MIT License. See [LICENSE](LICENSE) for details.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
> **Disclaimer** — PyProcore is an independent open-source project and is not affiliated with or endorsed by Procore Technologies.
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# PyProcore
|
|
2
|
+
|
|
3
|
+
> A production-ready Python SDK for building automation, integrations, and AI workflows on top of the Procore REST API.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/pyprocore/)
|
|
6
|
+
[](https://pypi.org/project/pyprocore/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/vibhanshu-mishra/pyprocore/actions/workflows/tests.yml)
|
|
9
|
+
[](#tests)
|
|
10
|
+
|
|
11
|
+
PyProcore handles the parts of a Procore integration that are tedious and easy to get wrong — OAuth, token refresh, pagination, retries, typed responses, structured logging, and attachment downloads — so you work with Python objects instead of raw JSON and API plumbing.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why PyProcore
|
|
16
|
+
|
|
17
|
+
Calling the Procore REST API directly means managing the OAuth handshake, refreshing expired tokens, following pagination headers, retrying failed requests, and parsing untyped JSON on every call.
|
|
18
|
+
|
|
19
|
+
PyProcore does that once, correctly, behind a clean interface. You call a service method and get back a typed Pydantic object. It is designed as the foundation layer for higher-level tools built on Procore data: engineering assistants, document analysis, workflow automation, and AI-powered review.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
**Authentication and transport**
|
|
26
|
+
|
|
27
|
+
- OAuth 2.0 authorization-code flow
|
|
28
|
+
- Automatic access-token refresh
|
|
29
|
+
- Automatic pagination via Procore response headers
|
|
30
|
+
- Request retries and structured logging with secret redaction
|
|
31
|
+
|
|
32
|
+
**API coverage (v1.0)**
|
|
33
|
+
|
|
34
|
+
- Companies
|
|
35
|
+
- Projects
|
|
36
|
+
- RFIs
|
|
37
|
+
- Submittals
|
|
38
|
+
- Attachment downloads
|
|
39
|
+
|
|
40
|
+
**Developer experience**
|
|
41
|
+
|
|
42
|
+
- Typed Pydantic response models
|
|
43
|
+
- Command-line interface
|
|
44
|
+
- 73 unit tests at 94% coverage, mocked with no live Procore dependency
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
| Package | Responsibility |
|
|
51
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
52
|
+
| `pyprocore/auth/` | OAuth exchange, token persistence, token refresh |
|
|
53
|
+
| `pyprocore/core/` | Configuration, endpoint paths, HTTP client, logging, exceptions |
|
|
54
|
+
| `pyprocore/models/` | Pydantic response models |
|
|
55
|
+
| `pyprocore/services/` | Company, project, RFI, submittal, and file services |
|
|
56
|
+
| `pyprocore/parser/` | Email parsing utilities for future automation |
|
|
57
|
+
| `tests/` | Mocked unit tests with no live Procore dependency |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Requires Python 3.12+.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip3 install pyprocore
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For local development:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/vibhanshu-mishra/pyprocore.git
|
|
73
|
+
cd pyprocore
|
|
74
|
+
python3 -m venv .venv
|
|
75
|
+
.venv/bin/python -m pip install --upgrade pip
|
|
76
|
+
.venv/bin/python -m pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Example
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pyprocore.services import list_projects
|
|
85
|
+
|
|
86
|
+
projects = list_projects(company_id=123456)
|
|
87
|
+
|
|
88
|
+
for project in projects:
|
|
89
|
+
print(project.name)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
Copy the example file and fill in real values:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cp .env.example .env
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Required variables:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
PROCORE_CLIENT_ID=your_client_id
|
|
106
|
+
PROCORE_CLIENT_SECRET=your_client_secret
|
|
107
|
+
PROCORE_REDIRECT_URI=http://localhost:8080/callback
|
|
108
|
+
PROCORE_LOGIN_URL=https://login.procore.com
|
|
109
|
+
PROCORE_API_BASE=https://api.procore.com
|
|
110
|
+
PROCORE_COMPANY_ID=123456
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Secrets, tokens, URLs, and company IDs are never hardcoded in source.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Authentication
|
|
118
|
+
|
|
119
|
+
Exchange the first authorization code and save the token locally:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pyprocore.auth.oauth import exchange_authorization_code
|
|
123
|
+
from pyprocore.auth.token_manager import TokenManager
|
|
124
|
+
|
|
125
|
+
token_response = exchange_authorization_code("authorization-code-from-procore")
|
|
126
|
+
TokenManager().save_oauth_response(token_response)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
After that, SDK clients read the token automatically:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from pyprocore.auth.token_manager import get_access_token
|
|
133
|
+
|
|
134
|
+
access_token = get_access_token()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Expired access tokens refresh automatically whenever a refresh token is available.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Usage
|
|
142
|
+
|
|
143
|
+
Once `.env` is configured and the one-time OAuth exchange is complete, calls return typed objects:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from pyprocore.services import list_projects
|
|
147
|
+
|
|
148
|
+
for project in list_projects(company_id=123456):
|
|
149
|
+
print(project.name)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Full service surface:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from pyprocore.services import (
|
|
156
|
+
download_rfi_attachments,
|
|
157
|
+
download_submittal_attachments,
|
|
158
|
+
get_rfi,
|
|
159
|
+
get_submittal,
|
|
160
|
+
list_companies,
|
|
161
|
+
list_projects,
|
|
162
|
+
list_rfis,
|
|
163
|
+
list_submittals,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
companies = list_companies()
|
|
167
|
+
projects = list_projects(company_id=123456)
|
|
168
|
+
|
|
169
|
+
rfis = list_rfis(project_id=352338)
|
|
170
|
+
rfi = get_rfi(project_id=352338, rfi_id=102784)
|
|
171
|
+
first_attachment_url = rfi.questions[0].attachments[0].url
|
|
172
|
+
|
|
173
|
+
submittals = list_submittals(project_id=352338)
|
|
174
|
+
submittal = get_submittal(project_id=352338, submittal_id=309641)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Every typed model serializes back to JSON:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
json_payload = rfi.model_dump(mode="json")
|
|
181
|
+
json_string = rfi.model_dump_json()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Downloading Attachments
|
|
187
|
+
|
|
188
|
+
Attachment URLs live at:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
RFI: questions[].attachments[].url
|
|
192
|
+
Submittal: attachments[].url
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Download through the service functions:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
rfi_files = download_rfi_attachments(project_id=352338, rfi_id=102784)
|
|
199
|
+
submittal_files = download_submittal_attachments(
|
|
200
|
+
project_id=352338,
|
|
201
|
+
submittal_id=309641,
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The shared file service supports safe filenames, streaming writes, retries, progress logging, batch downloads, and skip-existing behavior by default:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from pyprocore.services.files import FileDownloadService
|
|
209
|
+
|
|
210
|
+
files = FileDownloadService().download_attachments(
|
|
211
|
+
attachments,
|
|
212
|
+
"downloads/custom",
|
|
213
|
+
fallback_prefix="attachment",
|
|
214
|
+
overwrite=False,
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## CLI
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
procore-sdk companies
|
|
224
|
+
procore-sdk projects
|
|
225
|
+
procore-sdk rfis --project 352338
|
|
226
|
+
procore-sdk rfi --project 352338 --id 102784
|
|
227
|
+
procore-sdk submittals --project 352338
|
|
228
|
+
procore-sdk submittal --project 352338 --id 309641
|
|
229
|
+
procore-sdk download-rfi --project 352338 --id 102784
|
|
230
|
+
procore-sdk download-submittal --project 352338 --id 309641
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The CLI prints formatted JSON. Typed models are serialized with `model_dump(mode="json")`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Pagination
|
|
238
|
+
|
|
239
|
+
Collection methods use `ProcoreClient.get_all()`, which follows Procore pagination headers automatically. Business logic should call the service method or `get_all()` directly and never request page 2 by hand.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Logging
|
|
244
|
+
|
|
245
|
+
Structured logs are written to:
|
|
246
|
+
|
|
247
|
+
```text
|
|
248
|
+
logs/sdk.log
|
|
249
|
+
logs/errors.log
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Request logs record method, endpoint, status, elapsed time, and retry count. Exception logs record stack traces, exception type, request URL, HTTP status, and response body when available. The logger redacts sensitive keys such as authorization headers, access tokens, refresh tokens, and client secrets.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Implemented Endpoints (v1.0)
|
|
257
|
+
|
|
258
|
+
```text
|
|
259
|
+
GET /rest/v1.0/companies
|
|
260
|
+
GET /rest/v1.0/companies/{company_id}/projects
|
|
261
|
+
GET /rest/v1.1/projects/{project_id}/rfis
|
|
262
|
+
GET /rest/v1.1/projects/{project_id}/rfis/{rfi_id}
|
|
263
|
+
GET /rest/v1.1/projects/{project_id}/submittals
|
|
264
|
+
GET /rest/v1.1/projects/{project_id}/submittals/{submittal_id}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Roadmap
|
|
270
|
+
|
|
271
|
+
**Planned endpoints**
|
|
272
|
+
|
|
273
|
+
- Drawings
|
|
274
|
+
- Documents
|
|
275
|
+
- Specifications
|
|
276
|
+
- Daily Logs
|
|
277
|
+
- Photos
|
|
278
|
+
- Correspondence
|
|
279
|
+
- Observations
|
|
280
|
+
|
|
281
|
+
**Planned capabilities**
|
|
282
|
+
|
|
283
|
+
- AI workflow examples built on the SDK
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Troubleshooting
|
|
288
|
+
|
|
289
|
+
| Error | Likely cause and fix |
|
|
290
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
291
|
+
| `ConfigurationError` | `.env` is missing or a required key is absent. |
|
|
292
|
+
| `AuthenticationError` | Complete the first OAuth code exchange; confirm `pyprocore/auth/token_store.json` holds a refresh token. |
|
|
293
|
+
| `AuthorizationError` | The Procore user lacks access to the target company, project, or resource. |
|
|
294
|
+
| `ResourceNotFoundError` | Project, RFI, or submittal ID is wrong for the configured company. |
|
|
295
|
+
| Attachments not downloading | Check `logs/errors.log` for HTTP status and response body. Existing files are skipped unless `overwrite=True`. |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Tests
|
|
300
|
+
|
|
301
|
+
Run unit tests:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
.venv/bin/python -m unittest discover -s tests
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Run coverage:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
.venv/bin/python -m coverage run -m unittest discover -s tests
|
|
311
|
+
.venv/bin/python -m coverage report
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Supported Environments
|
|
317
|
+
|
|
318
|
+
- Procore Production
|
|
319
|
+
- Procore Sandbox
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Contributing
|
|
324
|
+
|
|
325
|
+
Contributions, issues, and feature requests are welcome. Please open an issue before submitting large changes.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
Released under the MIT License. See [LICENSE](LICENSE) for details.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
> **Disclaimer** — PyProcore is an independent open-source project and is not affiliated with or endorsed by Procore Technologies.
|