Skip to content

Commit 5db87bb

Browse files
btoronclaude
andcommitted
fix(statistics): fix PATCH serialization and response model for live API
- Add exclude_none=True to all 3 PATCH methods so null fields (keyId, resourceId) are not sent in the request body - Flatten StatisticsPatchResponse to match actual API response shape: {"status": "200", "updatedRecords": N} instead of nested {"results": {...}} - Remove StatisticsPatchResult model (not used by the actual API) - Fix airline distance roundtrip test to send 2 data points (required by API for company-level updates) with a safe +1 override delta All 54 statistics tests now pass (48 mocked + 6 live). Closes #142 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 49d30df commit 5db87bb

6 files changed

Lines changed: 773 additions & 10 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,15 @@ uv run pytest tests/async/test_async_workzones.py
404404

405405
### Statistics / Activity Duration Stats
406406
get_activity_duration_stats(self, resource_id=None, include_children=None, akey=None, offset=0, limit=100) [Async]
407+
update_activity_duration_stats(self, data) [Async]
407408

408409
### Statistics / Activity Travel Stats
409410
get_activity_travel_stats(self, region=None, tkey=None, fkey=None, key_id=None, offset=0, limit=100) [Async]
411+
update_activity_travel_stats(self, data) [Async]
410412

411413
### Statistics / Airline Distance Based Travel
412414
get_airline_distance_based_travel(self, level=None, key=None, distance=None, key_id=None, offset=0, limit=100) [Async]
415+
update_airline_distance_based_travel(self, data) [Async]
413416

414417
## Usage Examples
415418

docs/ENDPOINTS.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ This document provides a comprehensive reference of all Oracle Field Service Clo
105105
|ME058P|`/rest/ofscMetadata/v1/workZones/custom-actions/populateShapes` |metadata |POST |async |
106106
|ME059G|`/rest/ofscMetadata/v1/workZoneKey` |metadata |GET |- |
107107
|ST001G|`/rest/ofscStatistics/v1/activityDurationStats` |statistics |GET |async |
108-
|ST001A|`/rest/ofscStatistics/v1/activityDurationStats` |statistics |PATCH |- |
108+
|ST001A|`/rest/ofscStatistics/v1/activityDurationStats` |statistics |PATCH |async |
109109
|ST002G|`/rest/ofscStatistics/v1/activityTravelStats` |statistics |GET |async |
110-
|ST002A|`/rest/ofscStatistics/v1/activityTravelStats` |statistics |PATCH |- |
110+
|ST002A|`/rest/ofscStatistics/v1/activityTravelStats` |statistics |PATCH |async |
111111
|ST003G|`/rest/ofscStatistics/v1/airlineDistanceBasedTravel` |statistics |GET |async |
112-
|ST003A|`/rest/ofscStatistics/v1/airlineDistanceBasedTravel` |statistics |PATCH |- |
112+
|ST003A|`/rest/ofscStatistics/v1/airlineDistanceBasedTravel` |statistics |PATCH |async |
113113
|PC001U|`/rest/ofscPartsCatalog/v1/catalogs/{catalog}/{language}` |partscatalog |PUT |- |
114114
|PC002U|`/rest/ofscPartsCatalog/v1/catalogs/{catalog}/{language}/{itemLabel}` |partscatalog |PUT |- |
115115
|PC002D|`/rest/ofscPartsCatalog/v1/catalogs/{catalog}/{language}/{itemLabel}` |partscatalog |DELETE|- |
@@ -267,11 +267,11 @@ This document provides a comprehensive reference of all Oracle Field Service Clo
267267
## Implementation Summary
268268

269269
- **Sync only**: 4 endpoints
270-
- **Async only**: 92 endpoints
270+
- **Async only**: 95 endpoints
271271
- **Both**: 85 endpoints
272-
- **Not implemented**: 62 endpoints
272+
- **Not implemented**: 59 endpoints
273273
- **Total sync**: 89 endpoints
274-
- **Total async**: 177 endpoints
274+
- **Total async**: 180 endpoints
275275

276276
## Implementation Statistics by Module and Method
277277

@@ -295,11 +295,11 @@ This document provides a comprehensive reference of all Oracle Field Service Clo
295295
|metadata |41/51 (80.4%) |29/30 (96.7%) |5/5 (100.0%) |75/86 (87.2%) |
296296
|core |42/51 (82.4%) |30/56 (53.6%) |17/20 (85.0%) |89/127 (70.1%) |
297297
|capacity |6/7 (85.7%) |4/5 (80.0%) |0/0 (0%) |10/12 (83.3%) |
298-
|statistics |3/3 (100.0%) |0/3 (0.0%) |0/0 (0%) |3/6 (50.0%) |
298+
|statistics |3/3 (100.0%) |3/3 (100.0%) |0/0 (0%) |6/6 (100.0%) |
299299
|partscatalog |0/0 (0%) |0/2 (0.0%) |0/1 (0.0%) |0/3 (0.0%) |
300300
|collaboration|0/3 (0.0%) |0/4 (0.0%) |0/0 (0%) |0/7 (0.0%) |
301301
|auth |0/0 (0%) |0/2 (0.0%) |0/0 (0%) |0/2 (0.0%) |
302-
|**Total** |**92/115 (80.0%)**|**63/102 (61.8%)** |**22/26 (84.6%)**|**177/243 (72.8%)**|
302+
|**Total** |**92/115 (80.0%)**|**66/102 (64.7%)** |**22/26 (84.6%)**|**180/243 (74.1%)**|
303303

304304
## Endpoint ID Reference
305305

ofsc/async_client/statistics.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Async version of OFSC Statistics API module."""
22

3-
from typing import Optional
3+
from typing import Optional, Union
44
from urllib.parse import urljoin
55

66
import httpx
@@ -17,10 +17,14 @@
1717
OFSCValidationError,
1818
)
1919
from ..models import (
20+
ActivityDurationStatRequestList,
2021
ActivityDurationStatsList,
22+
ActivityTravelStatRequestList,
2123
ActivityTravelStatsList,
2224
AirlineDistanceBasedTravelList,
25+
AirlineDistanceBasedTravelRequestList,
2326
OFSConfig,
27+
StatisticsPatchResponse,
2428
)
2529

2630

@@ -279,3 +283,119 @@ async def get_airline_distance_based_travel(
279283
raise OFSCNetworkError(f"Network error: {str(e)}") from e
280284

281285
# endregion
286+
287+
# region Write Operations
288+
289+
async def update_activity_duration_stats(
290+
self,
291+
data: Union[ActivityDurationStatRequestList, dict],
292+
) -> StatisticsPatchResponse:
293+
"""Update activity duration statistics overrides.
294+
295+
Args:
296+
data: List of activity duration stat overrides to apply.
297+
298+
Returns:
299+
StatisticsPatchResponse: Result with status and updatedRecords count.
300+
301+
Raises:
302+
OFSCAuthenticationError: If authentication fails (401)
303+
OFSCAuthorizationError: If authorization fails (403)
304+
OFSCValidationError: If request data is invalid (400/422)
305+
OFSCApiError: For other API errors
306+
OFSCNetworkError: For network/transport errors
307+
"""
308+
if isinstance(data, dict):
309+
data = ActivityDurationStatRequestList.model_validate(data)
310+
url = urljoin(self.baseUrl, "/rest/ofscStatistics/v1/activityDurationStats")
311+
try:
312+
response = await self._client.patch(
313+
url,
314+
headers=self.headers,
315+
json=data.model_dump(mode="python", exclude_none=True),
316+
)
317+
response.raise_for_status()
318+
return StatisticsPatchResponse.model_validate(response.json())
319+
except httpx.HTTPStatusError as e:
320+
self._handle_http_error(e, "Failed to update activity duration stats")
321+
raise
322+
except httpx.TransportError as e:
323+
raise OFSCNetworkError(f"Network error: {str(e)}") from e
324+
325+
async def update_activity_travel_stats(
326+
self,
327+
data: Union[ActivityTravelStatRequestList, dict],
328+
) -> StatisticsPatchResponse:
329+
"""Update activity travel statistics overrides.
330+
331+
Args:
332+
data: List of activity travel stat overrides to apply.
333+
334+
Returns:
335+
StatisticsPatchResponse: Result with status and updatedRecords count.
336+
337+
Raises:
338+
OFSCAuthenticationError: If authentication fails (401)
339+
OFSCAuthorizationError: If authorization fails (403)
340+
OFSCConflictError: If "Detect activity travel keys automatically" is enabled (409)
341+
OFSCValidationError: If request data is invalid (400/422)
342+
OFSCApiError: For other API errors
343+
OFSCNetworkError: For network/transport errors
344+
"""
345+
if isinstance(data, dict):
346+
data = ActivityTravelStatRequestList.model_validate(data)
347+
url = urljoin(self.baseUrl, "/rest/ofscStatistics/v1/activityTravelStats")
348+
try:
349+
response = await self._client.patch(
350+
url,
351+
headers=self.headers,
352+
json=data.model_dump(mode="python", exclude_none=True),
353+
)
354+
response.raise_for_status()
355+
return StatisticsPatchResponse.model_validate(response.json())
356+
except httpx.HTTPStatusError as e:
357+
self._handle_http_error(e, "Failed to update activity travel stats")
358+
raise
359+
except httpx.TransportError as e:
360+
raise OFSCNetworkError(f"Network error: {str(e)}") from e
361+
362+
async def update_airline_distance_based_travel(
363+
self,
364+
data: Union[AirlineDistanceBasedTravelRequestList, dict],
365+
) -> StatisticsPatchResponse:
366+
"""Update airline distance based travel overrides.
367+
368+
Args:
369+
data: List of airline distance travel overrides to apply.
370+
371+
Returns:
372+
StatisticsPatchResponse: Result with status and updatedRecords count.
373+
374+
Raises:
375+
OFSCAuthenticationError: If authentication fails (401)
376+
OFSCAuthorizationError: If authorization fails (403)
377+
OFSCConflictError: If "Detect activity travel keys automatically" is enabled (409)
378+
OFSCValidationError: If request data is invalid (400/422)
379+
OFSCApiError: For other API errors
380+
OFSCNetworkError: For network/transport errors
381+
"""
382+
if isinstance(data, dict):
383+
data = AirlineDistanceBasedTravelRequestList.model_validate(data)
384+
url = urljoin(
385+
self.baseUrl, "/rest/ofscStatistics/v1/airlineDistanceBasedTravel"
386+
)
387+
try:
388+
response = await self._client.patch(
389+
url,
390+
headers=self.headers,
391+
json=data.model_dump(mode="python", exclude_none=True),
392+
)
393+
response.raise_for_status()
394+
return StatisticsPatchResponse.model_validate(response.json())
395+
except httpx.HTTPStatusError as e:
396+
self._handle_http_error(e, "Failed to update airline distance based travel")
397+
raise
398+
except httpx.TransportError as e:
399+
raise OFSCNetworkError(f"Network error: {str(e)}") from e
400+
401+
# endregion

ofsc/models/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,19 @@
189189
from .statistics import (
190190
ActivityDurationStat as ActivityDurationStat,
191191
ActivityDurationStatsList as ActivityDurationStatsList,
192+
ActivityDurationStatRequest as ActivityDurationStatRequest,
193+
ActivityDurationStatRequestList as ActivityDurationStatRequestList,
192194
ActivityTravelStat as ActivityTravelStat,
193195
ActivityTravelStatsList as ActivityTravelStatsList,
196+
ActivityTravelStatRequest as ActivityTravelStatRequest,
197+
ActivityTravelStatRequestList as ActivityTravelStatRequestList,
194198
AirlineDistanceData as AirlineDistanceData,
195199
AirlineDistanceBasedTravel as AirlineDistanceBasedTravel,
196200
AirlineDistanceBasedTravelList as AirlineDistanceBasedTravelList,
201+
AirlineDistanceOverrideData as AirlineDistanceOverrideData,
202+
AirlineDistanceBasedTravelRequest as AirlineDistanceBasedTravelRequest,
203+
AirlineDistanceBasedTravelRequestList as AirlineDistanceBasedTravelRequestList,
204+
StatisticsPatchResponse as StatisticsPatchResponse,
197205
)
198206

199207
# region Core / Activities

ofsc/models/statistics.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,88 @@ class AirlineDistanceBasedTravelList(OFSResponseList[AirlineDistanceBasedTravel]
7575

7676

7777
# endregion
78+
79+
80+
# region Statistics / Write Operations (shared PATCH response)
81+
82+
83+
class StatisticsPatchResponse(BaseModel):
84+
status: Optional[str] = None
85+
updatedRecords: Optional[int] = None
86+
87+
88+
# endregion
89+
90+
91+
# region Statistics / Activity Duration Stats Write
92+
93+
94+
class ActivityDurationStatRequest(BaseModel):
95+
resourceId: str = ""
96+
akey: str
97+
override: int
98+
99+
100+
class ActivityDurationStatRequestList(BaseModel):
101+
items: list[ActivityDurationStatRequest]
102+
103+
104+
# endregion
105+
106+
107+
# region Statistics / Activity Travel Stats Write
108+
109+
110+
class ActivityTravelStatRequest(BaseModel):
111+
fkey: str
112+
tkey: str
113+
override: int
114+
keyId: Optional[int] = None
115+
116+
117+
class ActivityTravelStatRequestList(BaseModel):
118+
items: list[ActivityTravelStatRequest]
119+
120+
121+
# endregion
122+
123+
124+
# region Statistics / Airline Distance Based Travel Write
125+
126+
127+
class AirlineDistanceOverrideData(BaseModel):
128+
distance: int
129+
override: int
130+
131+
132+
class AirlineDistanceBasedTravelRequest(BaseModel):
133+
data: list[AirlineDistanceOverrideData]
134+
key: Optional[str] = None
135+
keyId: Optional[int] = None
136+
level: Optional[str] = None
137+
138+
139+
class AirlineDistanceBasedTravelRequestList(BaseModel):
140+
items: list[AirlineDistanceBasedTravelRequest]
141+
142+
143+
# endregion
144+
145+
146+
__all__ = [
147+
"ActivityDurationStat",
148+
"ActivityDurationStatsList",
149+
"ActivityDurationStatRequest",
150+
"ActivityDurationStatRequestList",
151+
"ActivityTravelStat",
152+
"ActivityTravelStatsList",
153+
"ActivityTravelStatRequest",
154+
"ActivityTravelStatRequestList",
155+
"AirlineDistanceData",
156+
"AirlineDistanceBasedTravel",
157+
"AirlineDistanceBasedTravelList",
158+
"AirlineDistanceOverrideData",
159+
"AirlineDistanceBasedTravelRequest",
160+
"AirlineDistanceBasedTravelRequestList",
161+
"StatisticsPatchResponse",
162+
]

0 commit comments

Comments
 (0)