feat: rewrite SDK from camelCase/requests to snake_case/httpx/Pydantic v2
- Replace OnspringApiSdk package with onspring_api_sdk package - Add sync (OnspringClient) and async (AsyncOnspringClient) clients - Migrate from requests to httpx for HTTP transport - Migrate from raw dataclasses to Pydantic v2 models with alias support - Add shared response handler module (_responses.py) to reduce duplication - Add typed exceptions (OnspringError, OnspringAuthenticationError, etc.) - Preserve full API coverage for apps, fields, files, lists, records, and reports
This commit is contained in:
@@ -1,219 +0,0 @@
|
||||
import sys
|
||||
|
||||
import requests_mock
|
||||
|
||||
sys.path.append('src')
|
||||
|
||||
from OnspringApiSdk.OnspringClient import OnspringClient
|
||||
|
||||
|
||||
@requests_mock.Mocker(kw='mock')
|
||||
class TestOnspringClient(object):
|
||||
test_url = 'https://test.com'
|
||||
test_apiKey = 'apiKey'
|
||||
client = OnspringClient(test_url, test_apiKey)
|
||||
|
||||
def test_GetFieldsByAppId_WhenFieldContainsListValues_ItShouldReturnThoseValues(self, **kwargs):
|
||||
mockResponse = {
|
||||
"pageNumber": 1,
|
||||
"pageSize": 2,
|
||||
"totalPages": 1,
|
||||
"totalRecords": 2,
|
||||
"items": [
|
||||
{
|
||||
"multiplicity": "SingleSelect",
|
||||
"listId": 638,
|
||||
"values": [
|
||||
{
|
||||
"id": "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84",
|
||||
"name": "list_value_1",
|
||||
"sortOrder": 1,
|
||||
"numericValue": 1,
|
||||
"color": "#008e8e"
|
||||
},
|
||||
{
|
||||
"id": "0421e502-7f76-480a-9311-363aca3560bc",
|
||||
"name": "list_value_2",
|
||||
"sortOrder": 2,
|
||||
"numericValue": 2,
|
||||
"color": "#a186be"
|
||||
},
|
||||
{
|
||||
"id": "285b91c1-5800-47cb-a030-8cf7cdd7cdf1",
|
||||
"name": "updated_list_value_1676840661138}",
|
||||
"sortOrder": 3,
|
||||
"numericValue": 1,
|
||||
"color": "#000000"
|
||||
}
|
||||
],
|
||||
"id": 4801,
|
||||
"appId": 130,
|
||||
"name": "single_select_list_field",
|
||||
"type": "List",
|
||||
"status": "Enabled",
|
||||
"isRequired": True,
|
||||
"isUnique": False
|
||||
},
|
||||
{
|
||||
"outputType": "ListValue",
|
||||
"values": [
|
||||
{
|
||||
"id": "b235afb2-b786-4c87-bce9-fbd700e246c1",
|
||||
"name": "list_value_1",
|
||||
"sortOrder": 1,
|
||||
"numericValue": 1,
|
||||
"color": "#6dcff6"
|
||||
},
|
||||
{
|
||||
"id": "5cd7cd55-d6a6-40e0-a560-8aa407c13210",
|
||||
"name": "list_value_2",
|
||||
"sortOrder": 2,
|
||||
"numericValue": 2,
|
||||
"color": "#8e468e"
|
||||
}
|
||||
],
|
||||
"id": 4815,
|
||||
"appId": 130,
|
||||
"name": "list_formula_field",
|
||||
"type": "Formula",
|
||||
"status": "Enabled",
|
||||
"isRequired": False,
|
||||
"isUnique": False
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
kwargs['mock'].get(self.test_url + '/Fields/appId/1', json=mockResponse)
|
||||
|
||||
response = self.client.GetFieldsByAppId(1)
|
||||
|
||||
field_4801 = response.data.fields[0]
|
||||
field_4815 = response.data.fields[1]
|
||||
|
||||
assert field_4801.id == 4801, "Field id should be 4801"
|
||||
assert field_4801.listId == 638, "Field list id should be 638"
|
||||
assert field_4801.multiplicity == "SingleSelect", "Field multiplicity should be SingleSelect"
|
||||
assert field_4801.values is not None, "Field list values should not be None"
|
||||
|
||||
field_4801_value = field_4801.values[0]
|
||||
|
||||
assert field_4801_value.id == "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84", "Field value id should be 2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84"
|
||||
assert field_4801_value.name == "list_value_1", "Field value name should be list_value_1"
|
||||
assert field_4801_value.sortOrder == 1, "Field value sort order should be 1"
|
||||
assert field_4801_value.numericValue == 1, "Field value numeric value should be 1"
|
||||
assert field_4801_value.color == "#008e8e", "Field value color should be #008e8e"
|
||||
|
||||
assert field_4815.id == 4815, "Field id should be 4815"
|
||||
assert field_4815.listId is None, "Field list id should be None"
|
||||
assert field_4815.multiplicity is None, "Field multiplicity should be None"
|
||||
assert field_4815.values is not None, "Field list values should not be None"
|
||||
assert field_4815.outputType == 'ListValue', 'Field output type should be ListValue'
|
||||
|
||||
field_4815_value = field_4815.values[0]
|
||||
|
||||
assert field_4815_value.id == "b235afb2-b786-4c87-bce9-fbd700e246c1", "Field value id should be b235afb2-b786-4c87-bce9-fbd700e246c1"
|
||||
assert field_4815_value.name == "list_value_1", "Field value name should be list_value_1"
|
||||
assert field_4815_value.sortOrder == 1, "Field value sort order should be 1"
|
||||
assert field_4815_value.numericValue == 1, "Field value numeric value should be 1"
|
||||
assert field_4815_value.color == "#6dcff6", "Field value color should be #6dcff6"
|
||||
|
||||
def test_GetFieldsByIds_WhenFieldContainsListValues_ItShouldReturnThoseValues(self, **kwargs):
|
||||
mockResponse = {
|
||||
"count": 2,
|
||||
"items": [
|
||||
{
|
||||
"multiplicity": "SingleSelect",
|
||||
"listId": 638,
|
||||
"values": [
|
||||
{
|
||||
"id": "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84",
|
||||
"name": "list_value_1",
|
||||
"sortOrder": 1,
|
||||
"numericValue": 1,
|
||||
"color": "#008e8e"
|
||||
},
|
||||
{
|
||||
"id": "0421e502-7f76-480a-9311-363aca3560bc",
|
||||
"name": "list_value_2",
|
||||
"sortOrder": 2,
|
||||
"numericValue": 2,
|
||||
"color": "#a186be"
|
||||
},
|
||||
{
|
||||
"id": "285b91c1-5800-47cb-a030-8cf7cdd7cdf1",
|
||||
"name": "updated_list_value_1676840661138}",
|
||||
"sortOrder": 3,
|
||||
"numericValue": 1,
|
||||
"color": "#000000"
|
||||
}
|
||||
],
|
||||
"id": 4801,
|
||||
"appId": 130,
|
||||
"name": "single_select_list_field",
|
||||
"type": "List",
|
||||
"status": "Enabled",
|
||||
"isRequired": True,
|
||||
"isUnique": False
|
||||
},
|
||||
{
|
||||
"outputType": "ListValue",
|
||||
"values": [
|
||||
{
|
||||
"id": "b235afb2-b786-4c87-bce9-fbd700e246c1",
|
||||
"name": "list_value_1",
|
||||
"sortOrder": 1,
|
||||
"numericValue": 1,
|
||||
"color": "#6dcff6"
|
||||
},
|
||||
{
|
||||
"id": "5cd7cd55-d6a6-40e0-a560-8aa407c13210",
|
||||
"name": "list_value_2",
|
||||
"sortOrder": 2,
|
||||
"numericValue": 2,
|
||||
"color": "#8e468e"
|
||||
}
|
||||
],
|
||||
"id": 4815,
|
||||
"appId": 130,
|
||||
"name": "list_formula_field",
|
||||
"type": "Formula",
|
||||
"status": "Enabled",
|
||||
"isRequired": False,
|
||||
"isUnique": False
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
kwargs['mock'].post(self.test_url + '/Fields/batch-get', json=mockResponse)
|
||||
|
||||
response = self.client.GetFieldsByIds([4801, 4815])
|
||||
|
||||
field_4801 = response.data.fields[0]
|
||||
field_4815 = response.data.fields[1]
|
||||
|
||||
assert field_4801.id == 4801, "Field id should be 4801"
|
||||
assert field_4801.listId == 638, "Field list id should be 638"
|
||||
assert field_4801.multiplicity == "SingleSelect", "Field multiplicity should be SingleSelect"
|
||||
assert field_4801.values is not None, "Field list values should not be None"
|
||||
|
||||
field_4801_value = field_4801.values[0]
|
||||
|
||||
assert field_4801_value.id == "2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84", "Field value id should be 2c1af5b1-0f90-4378-b9a5-8b7e22f2bc84"
|
||||
assert field_4801_value.name == "list_value_1", "Field value name should be list_value_1"
|
||||
assert field_4801_value.sortOrder == 1, "Field value sort order should be 1"
|
||||
assert field_4801_value.numericValue == 1, "Field value numeric value should be 1"
|
||||
assert field_4801_value.color == "#008e8e", "Field value color should be #008e8e"
|
||||
|
||||
assert field_4815.id == 4815, "Field id should be 4815"
|
||||
assert field_4815.listId is None, "Field list id should be None"
|
||||
assert field_4815.multiplicity is None, "Field multiplicity should be None"
|
||||
assert field_4815.values is not None, "Field list values should not be None"
|
||||
assert field_4815.outputType == 'ListValue', 'Field output type should be ListValue'
|
||||
|
||||
field_4815_value = field_4815.values[0]
|
||||
|
||||
assert field_4815_value.id == "b235afb2-b786-4c87-bce9-fbd700e246c1", "Field value id should be b235afb2-b786-4c87-bce9-fbd700e246c1"
|
||||
assert field_4815_value.name == "list_value_1", "Field value name should be list_value_1"
|
||||
assert field_4815_value.sortOrder == 1, "Field value sort order should be 1"
|
||||
assert field_4815_value.numericValue == 1, "Field value numeric value should be 1"
|
||||
assert field_4815_value.color == "#6dcff6", "Field value color should be #6dcff6"
|
||||
@@ -1,323 +0,0 @@
|
||||
import uuid
|
||||
|
||||
# connectivity endpoints
|
||||
|
||||
def GetPingEndpoint(baseUrl: str) -> str:
|
||||
"""
|
||||
Returns the ping endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The ping endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Ping'
|
||||
|
||||
# app endpoints
|
||||
|
||||
def GetAppsEndpoint(baseUrl: str) -> str:
|
||||
"""
|
||||
Returns the get apps endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The get apps endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Apps'
|
||||
|
||||
def GetAppByIdEndpoint(baseUrl: str, appId: int) -> str:
|
||||
"""
|
||||
Returns the get app by id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app being requested.
|
||||
|
||||
Returns:
|
||||
The get app by id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Apps/id/{appId}'
|
||||
|
||||
def GetAppsByIdsEndpoint(baseUrl: str) -> str:
|
||||
"""
|
||||
Returns the get apps by ids endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The get apps by ids endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Apps/batch-get'
|
||||
|
||||
# field endpoints
|
||||
|
||||
def GetFieldByIdEndpoint(baseUrl: str, fieldId: int) -> str:
|
||||
"""
|
||||
Returns the get field by id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
fieldId (`int`): The id of the field being requested.
|
||||
|
||||
Returns:
|
||||
The get field by id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Fields/id/{fieldId}'
|
||||
|
||||
def GetFieldsByIdsEndpoint(baseUrl: str) -> str:
|
||||
"""
|
||||
Returns the get fields by ids endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The get fields by ids endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Fields/batch-get'
|
||||
|
||||
def GetFieldsByAppIdEndpoint(baseUrl: str, appId: int) -> str:
|
||||
"""
|
||||
Returns the get fields by app id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app whose fields are being requested.
|
||||
|
||||
Returns:
|
||||
The get fields by app id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Fields/appId/{appId}'
|
||||
|
||||
# file endpoints
|
||||
|
||||
def GetFileInfoByIdEndpoint(baseUrl: str, recordId: int, fieldId: int, fileId: int) -> str:
|
||||
"""
|
||||
Returns the get file info by its id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
recordId (`int`): The id for the record where the file resides.
|
||||
fieldId (`int`): The id for the field in the record where the file is held.
|
||||
fileId ('int'): The id of the file.
|
||||
|
||||
Returns:
|
||||
The get file info by its id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Files/recordId/{recordId}/fieldId/{fieldId}/fileId/{fileId}'
|
||||
|
||||
def DeleteFileByIdEndpoint(baseUrl: str, recordId: int, fieldId: int, fileId: int) -> str:
|
||||
"""
|
||||
Returns the delete file by its id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
recordId (`int`): The id for the record where the file resides.
|
||||
fieldId (`int`): The id for the field in the record where the file is held.
|
||||
fileId ('int'): The id of the file.
|
||||
|
||||
Returns:
|
||||
The delete file by its id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Files/recordId/{recordId}/fieldId/{fieldId}/fileId/{fileId}'
|
||||
|
||||
def GetFileByIdEndpoint(baseUrl: str, recordId: int, fieldId: int, fileId: int) -> str:
|
||||
"""
|
||||
Returns the get file by its id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
recordId (`int`): The id for the record where the file resides.
|
||||
fieldId (`int`): The id for the field in the record where the file is held.
|
||||
fileId ('int'): The id of the file.
|
||||
|
||||
Returns:
|
||||
The get file by its id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Files/recordId/{recordId}/fieldId/{fieldId}/fileId/{fileId}/file'
|
||||
|
||||
def SaveFileEndpoint(baseUrl: str) -> str:
|
||||
"""
|
||||
Returns the save file endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The save file endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Files'
|
||||
|
||||
# list endpoints
|
||||
|
||||
def AddOrUpdateListItemEndpoint(baseUrl, listId: int) -> str:
|
||||
"""
|
||||
Returns the add or update list item endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
listId (`int`): The id of the list for which the item belongs when being updated or being added to.
|
||||
|
||||
Returns:
|
||||
The add or update list item endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Lists/id/{listId}/items'
|
||||
|
||||
def DeleteListItemEndpoint(baseUrl: str, listId: int, itemId: uuid) -> str:
|
||||
"""
|
||||
Returns the delete list item endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
listId (`int`): The id of the list for which the list item belongs.
|
||||
itemId (`int`): The id list item.
|
||||
|
||||
Returns:
|
||||
The delete list item endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Lists/id/{listId}/itemId/{itemId}'
|
||||
|
||||
# record endpoints
|
||||
|
||||
def GetRecordsByAppIdEndpoint(baseUrl, appId: int) -> str:
|
||||
"""
|
||||
Returns the get records by app id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app where the records reside.
|
||||
|
||||
Returns:
|
||||
The get records by app id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/appId/{appId}'
|
||||
|
||||
def GetRecordByIdEndpoint(baseUrl, appId: int, recordId: int) -> str:
|
||||
"""
|
||||
Returns the get record by id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app where the record resides.
|
||||
recordId (`int`): The id of the record.
|
||||
|
||||
Returns:
|
||||
The get record by id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/appId/{appId}/recordId/{recordId}'
|
||||
|
||||
def DeleteRecordByIdEndpoint(baseUrl, appId: int, recordId: int) -> str:
|
||||
"""
|
||||
Returns the delete record by id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app where the record resides.
|
||||
recordId (`int`): The id of the record.
|
||||
|
||||
Returns:
|
||||
The delete record by id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/appId/{appId}/recordId/{recordId}'
|
||||
|
||||
def GetRecordsByIdsEndpoint(baseUrl) -> str:
|
||||
"""
|
||||
Returns the get records by ids endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The get records by ids endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/batch-get'
|
||||
|
||||
def QueryRecordsEndpoint(baseUrl) -> str:
|
||||
"""
|
||||
Returns the query records endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The query records endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/Query'
|
||||
|
||||
def AddOrUpdateRecordEndpoint(baseUrl) -> str:
|
||||
"""
|
||||
Returns the add or update record endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The add or update record endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records'
|
||||
|
||||
def DeleteRecordsByIds(baseUrl) -> str:
|
||||
"""
|
||||
Returns the delete records by ids endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
|
||||
Returns:
|
||||
The delete records by ids endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Records/batch-delete'
|
||||
|
||||
# report endpoints
|
||||
|
||||
def GetReportByIdEndpoint(baseUrl, reportId: int) -> str:
|
||||
"""
|
||||
Returns the get report by id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
reportId (`int`): The id of the report.
|
||||
|
||||
Returns:
|
||||
The get report by id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Reports/id/{reportId}'
|
||||
|
||||
def GetReportsByAppIdEndpoint(baseUrl, appId: int) -> str:
|
||||
"""
|
||||
Returns the get reports by app id endpoint.
|
||||
|
||||
Args:
|
||||
baseUrl (`str`): The base url for the api.
|
||||
appId (`int`): The id of the app where the reports reside.
|
||||
|
||||
Returns:
|
||||
The get reports by app id endpoint as a string.
|
||||
"""
|
||||
|
||||
return f'{baseUrl}/Reports/appId/{appId}'
|
||||
@@ -1,54 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
class DataFormat(Enum):
|
||||
"""
|
||||
The possible data format types for record field values.
|
||||
"""
|
||||
|
||||
Raw:int = 0
|
||||
Formatted:int = 1
|
||||
|
||||
class ReportDataType(Enum):
|
||||
"""
|
||||
The possible report data types for reports.
|
||||
"""
|
||||
ReportData:int = 0
|
||||
ChartData:int = 1
|
||||
|
||||
class ResultValueType(Enum):
|
||||
"""
|
||||
The possible types for record field values.
|
||||
"""
|
||||
String:int = 0
|
||||
Integer:int = 1
|
||||
Decimal:int = 2
|
||||
Date:int = 3
|
||||
TimeSpan:int = 4
|
||||
Guid:int = 5
|
||||
StringList:int = 6
|
||||
IntegerList:int = 7
|
||||
GuidList:int = 8
|
||||
AttachmentList:int = 9
|
||||
ScoringGroupList:int = 10
|
||||
FileList:int = 11
|
||||
|
||||
class Increment(Enum):
|
||||
"""
|
||||
The possible values for the increment property of timespan data in an Onspring timespan field.
|
||||
"""
|
||||
Seconds:str = "Second(s)"
|
||||
Minutes:str = "Minute(s)"
|
||||
Hours:str = "Hour(s)"
|
||||
Days:str = "Day(s)"
|
||||
Weeks:str = "Week(s)"
|
||||
Months:str = "Month(s)"
|
||||
Years:str = "Year(s)"
|
||||
|
||||
class Recurrence(Enum):
|
||||
"""
|
||||
The possible values for the recurrence property of timespan data in an Onspring timespan field.
|
||||
"""
|
||||
Empty:str = "None"
|
||||
EndByDate:str = "EndByDate"
|
||||
EndAfterOccurrences:str = 'EndAfterOccurrences'
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from datetime import datetime
|
||||
from OnspringApiSdk.Enums import *
|
||||
|
||||
def parseDate(date: str) -> datetime:
|
||||
|
||||
if date==None:
|
||||
return None
|
||||
|
||||
for format in ["%Y-%m-%dT%H:%M:%S.%fZ","%Y-%m-%dT%H:%M:%SZ"]:
|
||||
try:
|
||||
return datetime.strptime(date, format)
|
||||
except ValueError:
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
"""Onspring API SDK.
|
||||
|
||||
Provides sync and async clients for interacting with the Onspring API v2.
|
||||
"""
|
||||
|
||||
from onspring_api_sdk.async_client import AsyncOnspringClient
|
||||
from onspring_api_sdk.client import OnspringClient
|
||||
from onspring_api_sdk.errors import (
|
||||
OnspringAuthenticationError,
|
||||
OnspringError,
|
||||
OnspringNotFoundError,
|
||||
OnspringRateLimitError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"OnspringClient",
|
||||
"AsyncOnspringClient",
|
||||
"OnspringError",
|
||||
"OnspringAuthenticationError",
|
||||
"OnspringNotFoundError",
|
||||
"OnspringRateLimitError",
|
||||
]
|
||||
@@ -0,0 +1,734 @@
|
||||
"""Shared response handlers for Onspring API endpoints."""
|
||||
|
||||
import re
|
||||
|
||||
import httpx
|
||||
|
||||
from onspring_api_sdk.errors import _get_error_message
|
||||
from onspring_api_sdk.models import (
|
||||
AddOrUpdateListItemResponse,
|
||||
AddOrUpdateRecordResponse,
|
||||
ApiResponse,
|
||||
App,
|
||||
File,
|
||||
FileInfo,
|
||||
GetAppByIdResponse,
|
||||
GetAppsByIdsResponse,
|
||||
GetAppsResponse,
|
||||
GetBatchRecordsResponse,
|
||||
GetFieldByIdResponse,
|
||||
GetFieldsByAppIdResponse,
|
||||
GetFieldsByIdsResponse,
|
||||
GetFileByIdResponse,
|
||||
GetFileInfoByIdResponse,
|
||||
GetRecordsResponse,
|
||||
GetReportByIdResponse,
|
||||
GetReportsByAppIdResponse,
|
||||
OnspringField,
|
||||
Record,
|
||||
SaveFileResponse,
|
||||
)
|
||||
|
||||
|
||||
def handle_get_apps_response(response: httpx.Response) -> ApiResponse[GetAppsResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Invalid paging information",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetAppsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_field_by_id_response(response: httpx.Response) -> ApiResponse[GetFieldByIdResponse]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the field",
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Field could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetFieldByIdResponse(field=OnspringField.model_validate(response.json())),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_fields_by_ids_response(response: httpx.Response) -> ApiResponse[GetFieldsByIdsResponse]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the field(s)",
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Field(s) could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetFieldsByIdsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_fields_by_app_id_response(response: httpx.Response) -> ApiResponse[GetFieldsByAppIdResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Invalid paging information",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetFieldsByAppIdResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_apps_by_ids_response(response: httpx.Response) -> ApiResponse[GetAppsByIdsResponse]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the app",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetAppsByIdsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_app_by_id_response(response: httpx.Response) -> ApiResponse[GetAppByIdResponse]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the app",
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="App could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetAppByIdResponse(app=App.model_validate(response.json())),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_file_info_by_id_response(response: httpx.Response) -> ApiResponse[GetFileInfoByIdResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Request is invalid based on underlying data",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the file",
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="File could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetFileInfoByIdResponse(file_info=FileInfo.model_validate(response.json())),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_delete_file_by_id_response(response: httpx.Response) -> ApiResponse[None]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Request is invalid based on underlying data",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403 | 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 500:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="File could not be deleted due to internal error",
|
||||
raw_response=response,
|
||||
)
|
||||
case 204:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, message="File deleted successfully", raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_file_by_id_response(response: httpx.Response) -> ApiResponse[GetFileByIdResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Request is invalid based on underlying data",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403 | 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
headers = response.headers
|
||||
content_disposition = headers.get("content-disposition", "")
|
||||
match = re.search(r"filename=(.*?)(?:;|$)", content_disposition)
|
||||
file_name = match.group(1).strip("'\"") if match else "OnspringFile"
|
||||
file = File(
|
||||
name=file_name,
|
||||
contentType=headers.get("content-type", ""),
|
||||
contentLength=int(headers.get("content-length", 0)),
|
||||
content=response.content,
|
||||
)
|
||||
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, data=GetFileByIdResponse(file=file), raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_save_file_response(response: httpx.Response) -> ApiResponse[SaveFileResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Request is invalid based on underlying data",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403 | 404 | 500:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 201:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=SaveFileResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_add_or_update_list_item_response(
|
||||
response: httpx.Response,
|
||||
) -> ApiResponse[AddOrUpdateListItemResponse]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403 | 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 201:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=AddOrUpdateListItemResponse.model_validate(response.json()),
|
||||
message="New list value successfully added",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=AddOrUpdateListItemResponse.model_validate(response.json()),
|
||||
message="Existing list value successfully updated",
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_delete_list_item_response(response: httpx.Response) -> ApiResponse[None]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="List/item could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 204:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, message="Item deleted successfully", raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_records_by_app_id_response(response: httpx.Response) -> ApiResponse[GetRecordsResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Invalid paging information/size of the data requested was too large.",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetRecordsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_record_by_id_response(response: httpx.Response) -> ApiResponse[Record]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Record could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, data=Record.model_validate(response.json()), raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_delete_record_by_id_response(response: httpx.Response) -> ApiResponse[None]:
|
||||
match response.status_code:
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Record could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 204:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, message="Record deleted successfully", raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_records_by_ids_response(response: httpx.Response) -> ApiResponse[GetBatchRecordsResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Batch request is invalid/size of the data requested was too large.",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetBatchRecordsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_query_records_response(response: httpx.Response) -> ApiResponse[GetRecordsResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Query request is invalid/size of the data requested was too large.",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetRecordsResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_add_or_update_record_response(response: httpx.Response) -> ApiResponse[AddOrUpdateRecordResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Request data is invalid",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403 | 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200 | 201:
|
||||
message = "Record updated successfully" if response.status_code == 200 else "Record created successfully"
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=AddOrUpdateRecordResponse.model_validate(response.json()),
|
||||
message=message,
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_delete_records_by_ids_response(response: httpx.Response) -> ApiResponse[None]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Invalid request provided",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Records could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 204:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code, message="Record(s) deleted successfully", raw_response=response
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_report_by_id_response(response: httpx.Response) -> ApiResponse[GetReportByIdResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Invalid request based on underlying data",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 404:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Report could not be found",
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetReportByIdResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
|
||||
|
||||
def handle_get_reports_by_app_id_response(response: httpx.Response) -> ApiResponse[GetReportsByAppIdResponse]:
|
||||
match response.status_code:
|
||||
case 400:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Client does not have read access to the app.",
|
||||
raw_response=response,
|
||||
)
|
||||
case 401:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message="Unauthorized request",
|
||||
raw_response=response,
|
||||
)
|
||||
case 403:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
is_successful=False,
|
||||
message=_get_error_message(response),
|
||||
raw_response=response,
|
||||
)
|
||||
case 200:
|
||||
return ApiResponse(
|
||||
status_code=response.status_code,
|
||||
data=GetReportsByAppIdResponse.model_validate(response.json()),
|
||||
raw_response=response,
|
||||
)
|
||||
case _:
|
||||
return ApiResponse(status_code=response.status_code, is_successful=False, raw_response=response)
|
||||
@@ -0,0 +1,368 @@
|
||||
"""Async HTTP client for the Onspring API v2."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
||||
from onspring_api_sdk._responses import (
|
||||
handle_add_or_update_list_item_response,
|
||||
handle_add_or_update_record_response,
|
||||
handle_delete_file_by_id_response,
|
||||
handle_delete_list_item_response,
|
||||
handle_delete_record_by_id_response,
|
||||
handle_delete_records_by_ids_response,
|
||||
handle_get_app_by_id_response,
|
||||
handle_get_apps_by_ids_response,
|
||||
handle_get_apps_response,
|
||||
handle_get_field_by_id_response,
|
||||
handle_get_fields_by_app_id_response,
|
||||
handle_get_fields_by_ids_response,
|
||||
handle_get_file_by_id_response,
|
||||
handle_get_file_info_by_id_response,
|
||||
handle_get_record_by_id_response,
|
||||
handle_get_records_by_app_id_response,
|
||||
handle_get_records_by_ids_response,
|
||||
handle_get_report_by_id_response,
|
||||
handle_get_reports_by_app_id_response,
|
||||
handle_query_records_response,
|
||||
handle_save_file_response,
|
||||
)
|
||||
from onspring_api_sdk.endpoints import (
|
||||
add_or_update_list_item_endpoint,
|
||||
add_or_update_record_endpoint,
|
||||
delete_file_by_id_endpoint,
|
||||
delete_list_item_endpoint,
|
||||
delete_record_by_id_endpoint,
|
||||
delete_records_by_ids_endpoint,
|
||||
get_app_by_id_endpoint,
|
||||
get_apps_by_ids_endpoint,
|
||||
get_apps_endpoint,
|
||||
get_field_by_id_endpoint,
|
||||
get_fields_by_app_id_endpoint,
|
||||
get_fields_by_ids_endpoint,
|
||||
get_file_by_id_endpoint,
|
||||
get_file_info_by_id_endpoint,
|
||||
get_ping_endpoint,
|
||||
get_record_by_id_endpoint,
|
||||
get_records_by_app_id_endpoint,
|
||||
get_records_by_ids_endpoint,
|
||||
get_report_by_id_endpoint,
|
||||
get_reports_by_app_id_endpoint,
|
||||
query_records_endpoint,
|
||||
save_file_endpoint,
|
||||
)
|
||||
from onspring_api_sdk.models import (
|
||||
AddOrUpdateListItemResponse,
|
||||
AddOrUpdateRecordResponse,
|
||||
ApiResponse,
|
||||
DeleteBatchRecordsRequest,
|
||||
GetAppByIdResponse,
|
||||
GetAppsByIdsResponse,
|
||||
GetAppsResponse,
|
||||
GetBatchRecordsRequest,
|
||||
GetBatchRecordsResponse,
|
||||
GetFieldByIdResponse,
|
||||
GetFieldsByAppIdResponse,
|
||||
GetFieldsByIdsResponse,
|
||||
GetFileByIdResponse,
|
||||
GetFileInfoByIdResponse,
|
||||
GetRecordByIdRequest,
|
||||
GetRecordsByAppRequest,
|
||||
GetRecordsResponse,
|
||||
GetReportByIdRequest,
|
||||
GetReportByIdResponse,
|
||||
GetReportsByAppIdResponse,
|
||||
ListItemRequest,
|
||||
PagingRequest,
|
||||
QueryRecordsRequest,
|
||||
Record,
|
||||
SaveFileRequest,
|
||||
SaveFileResponse,
|
||||
)
|
||||
|
||||
API_VERSION = "2"
|
||||
CONTENT_TYPE_JSON = "application/json"
|
||||
_JSON_HEADERS: Final[Mapping[str, str]] = MappingProxyType({"Content-Type": CONTENT_TYPE_JSON})
|
||||
|
||||
|
||||
class AsyncOnspringClient:
|
||||
"""Async client for interacting with the Onspring API v2."""
|
||||
|
||||
def __init__(self, url: str, key: str):
|
||||
"""Initialize the client with a base URL and API key."""
|
||||
self.client = httpx.AsyncClient(
|
||||
headers={
|
||||
"x-apikey": key,
|
||||
"x-api-version": API_VERSION,
|
||||
}
|
||||
)
|
||||
self.base_url = url
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Close the underlying HTTP client."""
|
||||
await self.client.aclose()
|
||||
|
||||
async def __aenter__(self) -> "AsyncOnspringClient":
|
||||
"""Enter the async runtime context for the client."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args) -> None:
|
||||
"""Exit the async runtime context and close the client."""
|
||||
await self.aclose()
|
||||
|
||||
async def can_connect(self) -> bool:
|
||||
"""Ping the API to check connectivity."""
|
||||
response = await self.client.get(get_ping_endpoint(self.base_url))
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
async def get_apps(self, paging_request: PagingRequest | None = None) -> ApiResponse[GetAppsResponse]:
|
||||
"""Retrieve all apps the API key has access to."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = await self.client.get(
|
||||
get_apps_endpoint(self.base_url),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_apps_response(response)
|
||||
|
||||
async def get_app_by_id(self, app_id: int) -> ApiResponse[GetAppByIdResponse]:
|
||||
"""Retrieve an app by its ID."""
|
||||
response = await self.client.get(get_app_by_id_endpoint(self.base_url, app_id))
|
||||
|
||||
return handle_get_app_by_id_response(response)
|
||||
|
||||
async def get_apps_by_ids(self, app_ids: list[int]) -> ApiResponse[GetAppsByIdsResponse]:
|
||||
"""Retrieve multiple apps by their IDs."""
|
||||
if not isinstance(app_ids, (list, tuple)):
|
||||
return ApiResponse(status_code=400, is_successful=False, message="App ids should be of type list or tuple")
|
||||
|
||||
response = await self.client.post(
|
||||
get_apps_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(app_ids),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_apps_by_ids_response(response)
|
||||
|
||||
async def get_field_by_id(self, field_id: int) -> ApiResponse[GetFieldByIdResponse]:
|
||||
"""Retrieve a field by its ID."""
|
||||
response = await self.client.get(get_field_by_id_endpoint(self.base_url, field_id))
|
||||
|
||||
return handle_get_field_by_id_response(response)
|
||||
|
||||
async def get_fields_by_ids(self, field_ids: list[int]) -> ApiResponse[GetFieldsByIdsResponse]:
|
||||
"""Retrieve multiple fields by their IDs."""
|
||||
if not isinstance(field_ids, (list, tuple)):
|
||||
return ApiResponse(
|
||||
status_code=400, is_successful=False, message="Field ids should be of type list or tuple"
|
||||
)
|
||||
|
||||
response = await self.client.post(
|
||||
get_fields_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(field_ids),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_fields_by_ids_response(response)
|
||||
|
||||
async def get_fields_by_app_id(
|
||||
self, app_id: int, paging_request: PagingRequest | None = None
|
||||
) -> ApiResponse[GetFieldsByAppIdResponse]:
|
||||
"""Retrieve all fields for a given app."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = await self.client.get(
|
||||
get_fields_by_app_id_endpoint(self.base_url, app_id),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_fields_by_app_id_response(response)
|
||||
|
||||
async def get_file_info_by_id(
|
||||
self, record_id: int, field_id: int, file_id: int
|
||||
) -> ApiResponse[GetFileInfoByIdResponse]:
|
||||
"""Retrieve file metadata for a file attached to a record."""
|
||||
response = await self.client.get(get_file_info_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_get_file_info_by_id_response(response)
|
||||
|
||||
async def delete_file_by_id(self, record_id: int, field_id: int, file_id: int) -> ApiResponse[None]:
|
||||
"""Delete a file attached to a record."""
|
||||
response = await self.client.delete(delete_file_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_delete_file_by_id_response(response)
|
||||
|
||||
async def get_file_by_id(self, record_id: int, field_id: int, file_id: int) -> ApiResponse[GetFileByIdResponse]:
|
||||
"""Download a file attached to a record."""
|
||||
response = await self.client.get(get_file_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_get_file_by_id_response(response)
|
||||
|
||||
async def save_file(self, save_file_request: SaveFileRequest) -> ApiResponse[SaveFileResponse]:
|
||||
"""Upload a file to a record."""
|
||||
endpoint = save_file_endpoint(self.base_url)
|
||||
|
||||
def _read_file() -> bytes:
|
||||
with open(save_file_request.file_path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
file_content = await asyncio.to_thread(_read_file)
|
||||
|
||||
files = {
|
||||
"File": (
|
||||
save_file_request.file_name,
|
||||
file_content,
|
||||
save_file_request.content_type,
|
||||
),
|
||||
}
|
||||
|
||||
data = save_file_request.model_dump(
|
||||
by_alias=True, exclude={"file_name", "file_path", "content_type"}, exclude_none=True
|
||||
)
|
||||
|
||||
response = await self.client.post(endpoint, data=data, files=files)
|
||||
|
||||
return handle_save_file_response(response)
|
||||
|
||||
async def add_or_update_list_item(
|
||||
self, list_item_request: ListItemRequest
|
||||
) -> ApiResponse[AddOrUpdateListItemResponse]:
|
||||
"""Add or update a list item value."""
|
||||
endpoint = add_or_update_list_item_endpoint(self.base_url, list_item_request.list_id)
|
||||
payload = list_item_request.model_dump(by_alias=True, exclude={"list_id"}, exclude_none=True, mode="json")
|
||||
|
||||
response = await self.client.put(
|
||||
endpoint,
|
||||
content=json.dumps(payload),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_add_or_update_list_item_response(response)
|
||||
|
||||
async def delete_list_item(self, list_id: int, item_id: str) -> ApiResponse[None]:
|
||||
"""Delete a list item by its ID."""
|
||||
response = await self.client.delete(delete_list_item_endpoint(self.base_url, list_id, item_id))
|
||||
|
||||
return handle_delete_list_item_response(response)
|
||||
|
||||
async def get_records_by_app_id(self, request: GetRecordsByAppRequest) -> ApiResponse[GetRecordsResponse]:
|
||||
"""Retrieve records from an app with optional filtering and paging."""
|
||||
params = request.model_dump(by_alias=True, exclude={"app_id"}, exclude_none=True)
|
||||
field_ids = params.pop("fieldIds", None)
|
||||
|
||||
if field_ids:
|
||||
params["fieldIds"] = ",".join(str(i) for i in field_ids)
|
||||
|
||||
response = await self.client.get(
|
||||
get_records_by_app_id_endpoint(self.base_url, request.app_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_records_by_app_id_response(response)
|
||||
|
||||
async def get_record_by_id(self, request: GetRecordByIdRequest) -> ApiResponse[Record]:
|
||||
"""Retrieve a single record by its ID."""
|
||||
params = request.model_dump(by_alias=True, exclude={"app_id", "record_id"}, exclude_none=True)
|
||||
field_ids = params.pop("fieldIds", None)
|
||||
|
||||
if field_ids:
|
||||
params["fieldIds"] = ",".join(str(i) for i in field_ids)
|
||||
|
||||
response = await self.client.get(
|
||||
get_record_by_id_endpoint(self.base_url, request.app_id, request.record_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_record_by_id_response(response)
|
||||
|
||||
async def delete_record_by_id(self, app_id: int, record_id: int) -> ApiResponse[None]:
|
||||
"""Delete a single record by its ID."""
|
||||
response = await self.client.delete(delete_record_by_id_endpoint(self.base_url, app_id, record_id))
|
||||
|
||||
return handle_delete_record_by_id_response(response)
|
||||
|
||||
async def get_records_by_ids(self, request: GetBatchRecordsRequest) -> ApiResponse[GetBatchRecordsResponse]:
|
||||
"""Retrieve multiple records by their IDs."""
|
||||
response = await self.client.post(
|
||||
get_records_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(request.model_dump(by_alias=True, exclude_none=True, mode="json")),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_records_by_ids_response(response)
|
||||
|
||||
async def query_records(self, request: QueryRecordsRequest) -> ApiResponse[GetRecordsResponse]:
|
||||
"""Query records using a structured query."""
|
||||
exclude = {"page_number", "page_size"}
|
||||
payload = request.model_dump(by_alias=True, exclude=exclude, exclude_none=True, mode="json")
|
||||
params = {"pageNumber": request.page_number, "pageSize": request.page_size}
|
||||
|
||||
response = await self.client.post(
|
||||
query_records_endpoint(self.base_url),
|
||||
content=json.dumps(payload),
|
||||
params=params,
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_query_records_response(response)
|
||||
|
||||
async def add_or_update_record(self, record: Record) -> ApiResponse[AddOrUpdateRecordResponse]:
|
||||
"""Add or update a record."""
|
||||
fields_dict = {}
|
||||
|
||||
for field in record.fields:
|
||||
fields_dict[field.field_id] = field.value
|
||||
|
||||
payload = record.model_dump(by_alias=True, exclude={"fields"}, exclude_none=True, mode="json")
|
||||
payload["fields"] = fields_dict
|
||||
|
||||
response = await self.client.put(
|
||||
add_or_update_record_endpoint(self.base_url),
|
||||
content=json.dumps(payload, default=str),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_add_or_update_record_response(response)
|
||||
|
||||
async def delete_records_by_ids(self, request: DeleteBatchRecordsRequest) -> ApiResponse[None]:
|
||||
"""Delete multiple records by their IDs."""
|
||||
response = await self.client.post(
|
||||
delete_records_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(request.model_dump(by_alias=True, exclude_none=True, mode="json")),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_delete_records_by_ids_response(response)
|
||||
|
||||
async def get_report_by_id(self, request: GetReportByIdRequest) -> ApiResponse[GetReportByIdResponse]:
|
||||
"""Retrieve a report by its ID."""
|
||||
params = request.model_dump(by_alias=True, exclude={"report_id"}, exclude_none=True)
|
||||
|
||||
response = await self.client.get(
|
||||
get_report_by_id_endpoint(self.base_url, request.report_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_report_by_id_response(response)
|
||||
|
||||
async def get_reports_by_app_id(
|
||||
self, app_id: int, paging_request: PagingRequest | None = None
|
||||
) -> ApiResponse[GetReportsByAppIdResponse]:
|
||||
"""Retrieve all reports for a given app."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = await self.client.get(
|
||||
get_reports_by_app_id_endpoint(self.base_url, app_id),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_reports_by_app_id_response(response)
|
||||
@@ -0,0 +1,359 @@
|
||||
"""Sync HTTP client for the Onspring API v2."""
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
||||
from onspring_api_sdk._responses import (
|
||||
handle_add_or_update_list_item_response,
|
||||
handle_add_or_update_record_response,
|
||||
handle_delete_file_by_id_response,
|
||||
handle_delete_list_item_response,
|
||||
handle_delete_record_by_id_response,
|
||||
handle_delete_records_by_ids_response,
|
||||
handle_get_app_by_id_response,
|
||||
handle_get_apps_by_ids_response,
|
||||
handle_get_apps_response,
|
||||
handle_get_field_by_id_response,
|
||||
handle_get_fields_by_app_id_response,
|
||||
handle_get_fields_by_ids_response,
|
||||
handle_get_file_by_id_response,
|
||||
handle_get_file_info_by_id_response,
|
||||
handle_get_record_by_id_response,
|
||||
handle_get_records_by_app_id_response,
|
||||
handle_get_records_by_ids_response,
|
||||
handle_get_report_by_id_response,
|
||||
handle_get_reports_by_app_id_response,
|
||||
handle_query_records_response,
|
||||
handle_save_file_response,
|
||||
)
|
||||
from onspring_api_sdk.endpoints import (
|
||||
add_or_update_list_item_endpoint,
|
||||
add_or_update_record_endpoint,
|
||||
delete_file_by_id_endpoint,
|
||||
delete_list_item_endpoint,
|
||||
delete_record_by_id_endpoint,
|
||||
delete_records_by_ids_endpoint,
|
||||
get_app_by_id_endpoint,
|
||||
get_apps_by_ids_endpoint,
|
||||
get_apps_endpoint,
|
||||
get_field_by_id_endpoint,
|
||||
get_fields_by_app_id_endpoint,
|
||||
get_fields_by_ids_endpoint,
|
||||
get_file_by_id_endpoint,
|
||||
get_file_info_by_id_endpoint,
|
||||
get_ping_endpoint,
|
||||
get_record_by_id_endpoint,
|
||||
get_records_by_app_id_endpoint,
|
||||
get_records_by_ids_endpoint,
|
||||
get_report_by_id_endpoint,
|
||||
get_reports_by_app_id_endpoint,
|
||||
query_records_endpoint,
|
||||
save_file_endpoint,
|
||||
)
|
||||
from onspring_api_sdk.models import (
|
||||
AddOrUpdateListItemResponse,
|
||||
AddOrUpdateRecordResponse,
|
||||
ApiResponse,
|
||||
DeleteBatchRecordsRequest,
|
||||
GetAppByIdResponse,
|
||||
GetAppsByIdsResponse,
|
||||
GetAppsResponse,
|
||||
GetBatchRecordsRequest,
|
||||
GetBatchRecordsResponse,
|
||||
GetFieldByIdResponse,
|
||||
GetFieldsByAppIdResponse,
|
||||
GetFieldsByIdsResponse,
|
||||
GetFileByIdResponse,
|
||||
GetFileInfoByIdResponse,
|
||||
GetRecordByIdRequest,
|
||||
GetRecordsByAppRequest,
|
||||
GetRecordsResponse,
|
||||
GetReportByIdRequest,
|
||||
GetReportByIdResponse,
|
||||
GetReportsByAppIdResponse,
|
||||
ListItemRequest,
|
||||
PagingRequest,
|
||||
QueryRecordsRequest,
|
||||
Record,
|
||||
SaveFileRequest,
|
||||
SaveFileResponse,
|
||||
)
|
||||
|
||||
API_VERSION = "2"
|
||||
CONTENT_TYPE_JSON = "application/json"
|
||||
_JSON_HEADERS: Final[Mapping[str, str]] = MappingProxyType({"Content-Type": CONTENT_TYPE_JSON})
|
||||
|
||||
|
||||
class OnspringClient:
|
||||
"""Sync client for interacting with the Onspring API v2."""
|
||||
|
||||
def __init__(self, url: str, key: str):
|
||||
"""Initialize the client with a base URL and API key."""
|
||||
self.client = httpx.Client(
|
||||
headers={
|
||||
"x-apikey": key,
|
||||
"x-api-version": API_VERSION,
|
||||
}
|
||||
)
|
||||
self.base_url = url
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the underlying HTTP client."""
|
||||
self.client.close()
|
||||
|
||||
def __enter__(self) -> "OnspringClient":
|
||||
"""Enter the runtime context for the client."""
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
"""Exit the runtime context and close the client."""
|
||||
self.close()
|
||||
|
||||
def can_connect(self) -> bool:
|
||||
"""Ping the API to check connectivity."""
|
||||
response = self.client.get(get_ping_endpoint(self.base_url))
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
def get_apps(self, paging_request: PagingRequest | None = None) -> ApiResponse[GetAppsResponse]:
|
||||
"""Get all apps with optional paging."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = self.client.get(
|
||||
get_apps_endpoint(self.base_url),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_apps_response(response)
|
||||
|
||||
def get_app_by_id(self, app_id: int) -> ApiResponse[GetAppByIdResponse]:
|
||||
"""Get an app by its ID."""
|
||||
response = self.client.get(get_app_by_id_endpoint(self.base_url, app_id))
|
||||
|
||||
return handle_get_app_by_id_response(response)
|
||||
|
||||
def get_apps_by_ids(self, app_ids: list[int]) -> ApiResponse[GetAppsByIdsResponse]:
|
||||
"""Get multiple apps by their IDs."""
|
||||
if not isinstance(app_ids, (list, tuple)):
|
||||
return ApiResponse(status_code=400, is_successful=False, message="App ids should be of type list or tuple")
|
||||
|
||||
response = self.client.post(
|
||||
get_apps_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(app_ids),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_apps_by_ids_response(response)
|
||||
|
||||
def get_field_by_id(self, field_id: int) -> ApiResponse[GetFieldByIdResponse]:
|
||||
"""Get a field by its ID."""
|
||||
response = self.client.get(get_field_by_id_endpoint(self.base_url, field_id))
|
||||
|
||||
return handle_get_field_by_id_response(response)
|
||||
|
||||
def get_fields_by_ids(self, field_ids: list[int]) -> ApiResponse[GetFieldsByIdsResponse]:
|
||||
"""Get multiple fields by their IDs."""
|
||||
if not isinstance(field_ids, (list, tuple)):
|
||||
return ApiResponse(
|
||||
status_code=400, is_successful=False, message="Field ids should be of type list or tuple"
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
get_fields_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(field_ids),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_fields_by_ids_response(response)
|
||||
|
||||
def get_fields_by_app_id(
|
||||
self, app_id: int, paging_request: PagingRequest | None = None
|
||||
) -> ApiResponse[GetFieldsByAppIdResponse]:
|
||||
"""Get all fields for an app with optional paging."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = self.client.get(
|
||||
get_fields_by_app_id_endpoint(self.base_url, app_id),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_fields_by_app_id_response(response)
|
||||
|
||||
def get_file_info_by_id(self, record_id: int, field_id: int, file_id: int) -> ApiResponse[GetFileInfoByIdResponse]:
|
||||
"""Get file metadata by record, field, and file IDs."""
|
||||
response = self.client.get(get_file_info_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_get_file_info_by_id_response(response)
|
||||
|
||||
def delete_file_by_id(self, record_id: int, field_id: int, file_id: int) -> ApiResponse[None]:
|
||||
"""Delete a file by record, field, and file IDs."""
|
||||
response = self.client.delete(delete_file_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_delete_file_by_id_response(response)
|
||||
|
||||
def get_file_by_id(self, record_id: int, field_id: int, file_id: int) -> ApiResponse[GetFileByIdResponse]:
|
||||
"""Get a file by record, field, and file IDs."""
|
||||
response = self.client.get(get_file_by_id_endpoint(self.base_url, record_id, field_id, file_id))
|
||||
|
||||
return handle_get_file_by_id_response(response)
|
||||
|
||||
def save_file(self, save_file_request: SaveFileRequest) -> ApiResponse[SaveFileResponse]:
|
||||
"""Save a file to a record."""
|
||||
endpoint = save_file_endpoint(self.base_url)
|
||||
|
||||
with open(save_file_request.file_path, "rb") as f:
|
||||
file_content = f.read()
|
||||
|
||||
files = {
|
||||
"File": (
|
||||
save_file_request.file_name,
|
||||
file_content,
|
||||
save_file_request.content_type,
|
||||
),
|
||||
}
|
||||
|
||||
data = save_file_request.model_dump(
|
||||
by_alias=True, exclude={"file_name", "file_path", "content_type"}, exclude_none=True
|
||||
)
|
||||
|
||||
response = self.client.post(endpoint, data=data, files=files)
|
||||
|
||||
return handle_save_file_response(response)
|
||||
|
||||
def add_or_update_list_item(self, list_item_request: ListItemRequest) -> ApiResponse[AddOrUpdateListItemResponse]:
|
||||
"""Add or update a list item value."""
|
||||
endpoint = add_or_update_list_item_endpoint(self.base_url, list_item_request.list_id)
|
||||
payload = list_item_request.model_dump(by_alias=True, exclude={"list_id"}, exclude_none=True, mode="json")
|
||||
|
||||
response = self.client.put(
|
||||
endpoint,
|
||||
content=json.dumps(payload),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_add_or_update_list_item_response(response)
|
||||
|
||||
def delete_list_item(self, list_id: int, item_id: str) -> ApiResponse[None]:
|
||||
"""Delete a list item by list and item IDs."""
|
||||
response = self.client.delete(delete_list_item_endpoint(self.base_url, list_id, item_id))
|
||||
|
||||
return handle_delete_list_item_response(response)
|
||||
|
||||
def get_records_by_app_id(self, request: GetRecordsByAppRequest) -> ApiResponse[GetRecordsResponse]:
|
||||
"""Get records for an app with optional filtering and paging."""
|
||||
params = request.model_dump(by_alias=True, exclude={"app_id"}, exclude_none=True)
|
||||
field_ids = params.pop("fieldIds", None)
|
||||
if field_ids:
|
||||
params["fieldIds"] = ",".join(str(i) for i in field_ids)
|
||||
|
||||
response = self.client.get(
|
||||
get_records_by_app_id_endpoint(self.base_url, request.app_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_records_by_app_id_response(response)
|
||||
|
||||
def get_record_by_id(self, request: GetRecordByIdRequest) -> ApiResponse[Record]:
|
||||
"""Get a record by its app and record IDs."""
|
||||
params = request.model_dump(by_alias=True, exclude={"app_id", "record_id"}, exclude_none=True)
|
||||
field_ids = params.pop("fieldIds", None)
|
||||
|
||||
if field_ids:
|
||||
params["fieldIds"] = ",".join(str(i) for i in field_ids)
|
||||
|
||||
response = self.client.get(
|
||||
get_record_by_id_endpoint(self.base_url, request.app_id, request.record_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_record_by_id_response(response)
|
||||
|
||||
def delete_record_by_id(self, app_id: int, record_id: int) -> ApiResponse[None]:
|
||||
"""Delete a record by its app and record IDs."""
|
||||
response = self.client.delete(delete_record_by_id_endpoint(self.base_url, app_id, record_id))
|
||||
|
||||
return handle_delete_record_by_id_response(response)
|
||||
|
||||
def get_records_by_ids(self, request: GetBatchRecordsRequest) -> ApiResponse[GetBatchRecordsResponse]:
|
||||
"""Get multiple records by their IDs."""
|
||||
response = self.client.post(
|
||||
get_records_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(request.model_dump(by_alias=True, exclude_none=True, mode="json")),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_get_records_by_ids_response(response)
|
||||
|
||||
def query_records(self, request: QueryRecordsRequest) -> ApiResponse[GetRecordsResponse]:
|
||||
"""Query records using a structured query request."""
|
||||
exclude = {"page_number", "page_size"}
|
||||
payload = request.model_dump(by_alias=True, exclude=exclude, exclude_none=True, mode="json")
|
||||
params = {"pageNumber": request.page_number, "pageSize": request.page_size}
|
||||
|
||||
response = self.client.post(
|
||||
query_records_endpoint(self.base_url),
|
||||
content=json.dumps(payload),
|
||||
params=params,
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_query_records_response(response)
|
||||
|
||||
def add_or_update_record(self, record: Record) -> ApiResponse[AddOrUpdateRecordResponse]:
|
||||
"""Add or update a record."""
|
||||
fields_dict = {}
|
||||
|
||||
for field in record.fields:
|
||||
fields_dict[field.field_id] = field.value
|
||||
|
||||
payload = record.model_dump(by_alias=True, exclude={"fields"}, exclude_none=True, mode="json")
|
||||
payload["fields"] = fields_dict
|
||||
|
||||
response = self.client.put(
|
||||
add_or_update_record_endpoint(self.base_url),
|
||||
content=json.dumps(payload, default=str),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_add_or_update_record_response(response)
|
||||
|
||||
def delete_records_by_ids(self, request: DeleteBatchRecordsRequest) -> ApiResponse[None]:
|
||||
"""Delete multiple records by their IDs."""
|
||||
response = self.client.post(
|
||||
delete_records_by_ids_endpoint(self.base_url),
|
||||
content=json.dumps(request.model_dump(by_alias=True, exclude_none=True, mode="json")),
|
||||
headers=_JSON_HEADERS,
|
||||
)
|
||||
|
||||
return handle_delete_records_by_ids_response(response)
|
||||
|
||||
def get_report_by_id(self, request: GetReportByIdRequest) -> ApiResponse[GetReportByIdResponse]:
|
||||
"""Get a report by its ID."""
|
||||
params = request.model_dump(by_alias=True, exclude={"report_id"}, exclude_none=True)
|
||||
|
||||
response = self.client.get(
|
||||
get_report_by_id_endpoint(self.base_url, request.report_id),
|
||||
params=params,
|
||||
)
|
||||
|
||||
return handle_get_report_by_id_response(response)
|
||||
|
||||
def get_reports_by_app_id(
|
||||
self, app_id: int, paging_request: PagingRequest | None = None
|
||||
) -> ApiResponse[GetReportsByAppIdResponse]:
|
||||
"""Get all reports for an app with optional paging."""
|
||||
if paging_request is None:
|
||||
paging_request = PagingRequest()
|
||||
|
||||
response = self.client.get(
|
||||
get_reports_by_app_id_endpoint(self.base_url, app_id),
|
||||
params=paging_request.model_dump(by_alias=True, exclude_none=True),
|
||||
)
|
||||
|
||||
return handle_get_reports_by_app_id_response(response)
|
||||
@@ -0,0 +1,111 @@
|
||||
"""URL builder functions for all Onspring API v2 endpoints."""
|
||||
|
||||
|
||||
def get_ping_endpoint(base_url: str) -> str:
|
||||
"""Build the ping endpoint URL."""
|
||||
return f"{base_url}/Ping"
|
||||
|
||||
|
||||
def get_apps_endpoint(base_url: str) -> str:
|
||||
"""Build the get-apps endpoint URL."""
|
||||
return f"{base_url}/Apps"
|
||||
|
||||
|
||||
def get_app_by_id_endpoint(base_url: str, app_id: int) -> str:
|
||||
"""Build the get-app-by-id endpoint URL."""
|
||||
return f"{base_url}/Apps/id/{app_id}"
|
||||
|
||||
|
||||
def get_apps_by_ids_endpoint(base_url: str) -> str:
|
||||
"""Build the get-apps-by-ids endpoint URL."""
|
||||
return f"{base_url}/Apps/batch-get"
|
||||
|
||||
|
||||
def get_field_by_id_endpoint(base_url: str, field_id: int) -> str:
|
||||
"""Build the get-field-by-id endpoint URL."""
|
||||
return f"{base_url}/Fields/id/{field_id}"
|
||||
|
||||
|
||||
def get_fields_by_ids_endpoint(base_url: str) -> str:
|
||||
"""Build the get-fields-by-ids endpoint URL."""
|
||||
return f"{base_url}/Fields/batch-get"
|
||||
|
||||
|
||||
def get_fields_by_app_id_endpoint(base_url: str, app_id: int) -> str:
|
||||
"""Build the get-fields-by-app-id endpoint URL."""
|
||||
return f"{base_url}/Fields/appId/{app_id}"
|
||||
|
||||
|
||||
def get_file_info_by_id_endpoint(base_url: str, record_id: int, field_id: int, file_id: int) -> str:
|
||||
"""Build the get-file-info-by-id endpoint URL."""
|
||||
return f"{base_url}/Files/recordId/{record_id}/fieldId/{field_id}/fileId/{file_id}"
|
||||
|
||||
|
||||
def delete_file_by_id_endpoint(base_url: str, record_id: int, field_id: int, file_id: int) -> str:
|
||||
"""Build the delete-file-by-id endpoint URL."""
|
||||
return f"{base_url}/Files/recordId/{record_id}/fieldId/{field_id}/fileId/{file_id}"
|
||||
|
||||
|
||||
def get_file_by_id_endpoint(base_url: str, record_id: int, field_id: int, file_id: int) -> str:
|
||||
"""Build the get-file-by-id endpoint URL."""
|
||||
return f"{base_url}/Files/recordId/{record_id}/fieldId/{field_id}/fileId/{file_id}/file"
|
||||
|
||||
|
||||
def save_file_endpoint(base_url: str) -> str:
|
||||
"""Build the save-file endpoint URL."""
|
||||
return f"{base_url}/Files"
|
||||
|
||||
|
||||
def add_or_update_list_item_endpoint(base_url: str, list_id: int) -> str:
|
||||
"""Build the add-or-update-list-item endpoint URL."""
|
||||
return f"{base_url}/Lists/id/{list_id}/items"
|
||||
|
||||
|
||||
def delete_list_item_endpoint(base_url: str, list_id: int, item_id: str) -> str:
|
||||
"""Build the delete-list-item endpoint URL."""
|
||||
return f"{base_url}/Lists/id/{list_id}/itemId/{item_id}"
|
||||
|
||||
|
||||
def get_records_by_app_id_endpoint(base_url: str, app_id: int) -> str:
|
||||
"""Build the get-records-by-app-id endpoint URL."""
|
||||
return f"{base_url}/Records/appId/{app_id}"
|
||||
|
||||
|
||||
def get_record_by_id_endpoint(base_url: str, app_id: int, record_id: int) -> str:
|
||||
"""Build the get-record-by-id endpoint URL."""
|
||||
return f"{base_url}/Records/appId/{app_id}/recordId/{record_id}"
|
||||
|
||||
|
||||
def delete_record_by_id_endpoint(base_url: str, app_id: int, record_id: int) -> str:
|
||||
"""Build the delete-record-by-id endpoint URL."""
|
||||
return f"{base_url}/Records/appId/{app_id}/recordId/{record_id}"
|
||||
|
||||
|
||||
def get_records_by_ids_endpoint(base_url: str) -> str:
|
||||
"""Build the get-records-by-ids endpoint URL."""
|
||||
return f"{base_url}/Records/batch-get"
|
||||
|
||||
|
||||
def query_records_endpoint(base_url: str) -> str:
|
||||
"""Build the query-records endpoint URL."""
|
||||
return f"{base_url}/Records/Query"
|
||||
|
||||
|
||||
def add_or_update_record_endpoint(base_url: str) -> str:
|
||||
"""Build the add-or-update-record endpoint URL."""
|
||||
return f"{base_url}/Records"
|
||||
|
||||
|
||||
def delete_records_by_ids_endpoint(base_url: str) -> str:
|
||||
"""Build the delete-records-by-ids endpoint URL."""
|
||||
return f"{base_url}/Records/batch-delete"
|
||||
|
||||
|
||||
def get_report_by_id_endpoint(base_url: str, report_id: int) -> str:
|
||||
"""Build the get-report-by-id endpoint URL."""
|
||||
return f"{base_url}/Reports/id/{report_id}"
|
||||
|
||||
|
||||
def get_reports_by_app_id_endpoint(base_url: str, app_id: int) -> str:
|
||||
"""Build the get-reports-by-app-id endpoint URL."""
|
||||
return f"{base_url}/Reports/appId/{app_id}"
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Enumerations for Onspring API data types and timespan configuration."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DataFormat(Enum):
|
||||
"""The possible data format types for record field values."""
|
||||
|
||||
Raw: int = 0
|
||||
Formatted: int = 1
|
||||
|
||||
|
||||
class ReportDataType(Enum):
|
||||
"""The possible report data types for reports."""
|
||||
|
||||
ReportData: int = 0
|
||||
ChartData: int = 1
|
||||
|
||||
|
||||
class ResultValueType(Enum):
|
||||
"""The possible types for record field values."""
|
||||
|
||||
String: int = 0
|
||||
Integer: int = 1
|
||||
Decimal: int = 2
|
||||
Date: int = 3
|
||||
TimeSpan: int = 4
|
||||
Guid: int = 5
|
||||
StringList: int = 6
|
||||
IntegerList: int = 7
|
||||
GuidList: int = 8
|
||||
AttachmentList: int = 9
|
||||
ScoringGroupList: int = 10
|
||||
FileList: int = 11
|
||||
|
||||
|
||||
class Increment(Enum):
|
||||
"""Possible increment values for timespan data in an Onspring timespan field."""
|
||||
|
||||
Seconds: str = "Second(s)"
|
||||
Minutes: str = "Minute(s)"
|
||||
Hours: str = "Hour(s)"
|
||||
Days: str = "Day(s)"
|
||||
Weeks: str = "Week(s)"
|
||||
Months: str = "Month(s)"
|
||||
Years: str = "Year(s)"
|
||||
|
||||
|
||||
class Recurrence(Enum):
|
||||
"""Possible recurrence values for timespan data in an Onspring timespan field."""
|
||||
|
||||
Empty: str = "None"
|
||||
EndByDate: str = "EndByDate"
|
||||
EndAfterOccurrences: str = "EndAfterOccurrences"
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Custom exception hierarchy for Onspring API errors."""
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def _get_error_message(response: httpx.Response) -> str | None:
|
||||
"""Safely extract an error message from an API response body."""
|
||||
try:
|
||||
body = response.json()
|
||||
if isinstance(body, dict):
|
||||
return body.get("message")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class OnspringError(Exception):
|
||||
"""Base exception for all Onspring API errors."""
|
||||
|
||||
|
||||
class OnspringAuthenticationError(OnspringError):
|
||||
"""Raised when the API returns a 401 or 403 status code."""
|
||||
|
||||
|
||||
class OnspringNotFoundError(OnspringError):
|
||||
"""Raised when the API returns a 404 status code."""
|
||||
|
||||
|
||||
class OnspringRateLimitError(OnspringError):
|
||||
"""Raised when the API returns a 429 status code."""
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Re-exports for all Pydantic models used by the Onspring API SDK."""
|
||||
|
||||
from onspring_api_sdk.models.app import App, GetAppByIdResponse, GetAppsByIdsResponse, GetAppsResponse
|
||||
from onspring_api_sdk.models.common import ApiResponse, PagingRequest
|
||||
from onspring_api_sdk.models.field import (
|
||||
GetFieldByIdResponse,
|
||||
GetFieldsByAppIdResponse,
|
||||
GetFieldsByIdsResponse,
|
||||
ListValue,
|
||||
OnspringField,
|
||||
)
|
||||
from onspring_api_sdk.models.file import (
|
||||
File,
|
||||
FileInfo,
|
||||
GetFileByIdResponse,
|
||||
GetFileInfoByIdResponse,
|
||||
SaveFileRequest,
|
||||
SaveFileResponse,
|
||||
)
|
||||
from onspring_api_sdk.models.list import AddOrUpdateListItemResponse, ListItemRequest
|
||||
from onspring_api_sdk.models.record import (
|
||||
AddOrUpdateRecordResponse,
|
||||
Attachment,
|
||||
AttachmentListValue,
|
||||
DateFieldValue,
|
||||
DecimalFieldValue,
|
||||
DeleteBatchRecordsRequest,
|
||||
FileListValue,
|
||||
GetBatchRecordsRequest,
|
||||
GetBatchRecordsResponse,
|
||||
GetRecordByIdRequest,
|
||||
GetRecordsByAppRequest,
|
||||
GetRecordsResponse,
|
||||
GuidFieldValue,
|
||||
GuidListValue,
|
||||
IntegerFieldValue,
|
||||
IntegerListValue,
|
||||
QueryRecordsRequest,
|
||||
Record,
|
||||
RecordFieldValue,
|
||||
ScoringGroup,
|
||||
ScoringGroupListValue,
|
||||
StringFieldValue,
|
||||
StringListValue,
|
||||
TimeSpanData,
|
||||
TimeSpanValue,
|
||||
)
|
||||
from onspring_api_sdk.models.report import (
|
||||
GetReportByIdRequest,
|
||||
GetReportByIdResponse,
|
||||
GetReportsByAppIdResponse,
|
||||
Report,
|
||||
Row,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ApiResponse",
|
||||
"PagingRequest",
|
||||
"App",
|
||||
"GetAppsResponse",
|
||||
"GetAppByIdResponse",
|
||||
"GetAppsByIdsResponse",
|
||||
"ListValue",
|
||||
"OnspringField",
|
||||
"GetFieldByIdResponse",
|
||||
"GetFieldsByIdsResponse",
|
||||
"GetFieldsByAppIdResponse",
|
||||
"RecordFieldValue",
|
||||
"StringFieldValue",
|
||||
"IntegerFieldValue",
|
||||
"DecimalFieldValue",
|
||||
"DateFieldValue",
|
||||
"GuidFieldValue",
|
||||
"TimeSpanData",
|
||||
"TimeSpanValue",
|
||||
"StringListValue",
|
||||
"IntegerListValue",
|
||||
"GuidListValue",
|
||||
"Attachment",
|
||||
"AttachmentListValue",
|
||||
"ScoringGroup",
|
||||
"ScoringGroupListValue",
|
||||
"FileListValue",
|
||||
"Record",
|
||||
"GetRecordsByAppRequest",
|
||||
"QueryRecordsRequest",
|
||||
"GetRecordsResponse",
|
||||
"GetRecordByIdRequest",
|
||||
"GetBatchRecordsRequest",
|
||||
"GetBatchRecordsResponse",
|
||||
"AddOrUpdateRecordResponse",
|
||||
"DeleteBatchRecordsRequest",
|
||||
"Row",
|
||||
"Report",
|
||||
"GetReportByIdRequest",
|
||||
"GetReportByIdResponse",
|
||||
"GetReportsByAppIdResponse",
|
||||
"File",
|
||||
"FileInfo",
|
||||
"GetFileInfoByIdResponse",
|
||||
"GetFileByIdResponse",
|
||||
"SaveFileRequest",
|
||||
"SaveFileResponse",
|
||||
"ListItemRequest",
|
||||
"AddOrUpdateListItemResponse",
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Pydantic models for Onspring app API responses."""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class App(BaseModel):
|
||||
"""Represents an Onspring app."""
|
||||
|
||||
href: str
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class GetAppsResponse(BaseModel):
|
||||
"""Paginated response containing a list of apps."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page_number: int = Field(alias="pageNumber")
|
||||
page_size: int = Field(alias="pageSize")
|
||||
total_pages: int = Field(alias="totalPages")
|
||||
total_records: int = Field(alias="totalRecords")
|
||||
apps: list[App] = Field(alias="items")
|
||||
|
||||
|
||||
class GetAppByIdResponse(BaseModel):
|
||||
"""Response containing a single app."""
|
||||
|
||||
app: App
|
||||
|
||||
|
||||
class GetAppsByIdsResponse(BaseModel):
|
||||
"""Response containing a list of apps for requested IDs."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
count: int
|
||||
apps: list[App] = Field(alias="items")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Shared Pydantic models for paging and generic API responses."""
|
||||
|
||||
from typing import Generic, Optional, TypeVar
|
||||
|
||||
from httpx import Response
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from onspring_api_sdk.errors import (
|
||||
OnspringAuthenticationError,
|
||||
OnspringError,
|
||||
OnspringNotFoundError,
|
||||
OnspringRateLimitError,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class PagingRequest(BaseModel):
|
||||
"""Paging parameters for paginated API requests."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page_number: int = Field(alias="pageNumber", default=1)
|
||||
page_size: int = Field(alias="pageSize", default=50)
|
||||
|
||||
|
||||
class ApiResponse(BaseModel, Generic[T]):
|
||||
"""Generic wrapper for all API responses with status and error handling."""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
status_code: int
|
||||
is_successful: bool = True
|
||||
message: Optional[str] = None
|
||||
data: Optional[T] = None
|
||||
raw_response: Optional[Response] = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def set_is_successful(cls, data):
|
||||
"""Automatically infer is_successful from status_code if not provided."""
|
||||
if isinstance(data, dict) and "is_successful" not in data and "status_code" in data:
|
||||
data["is_successful"] = int(data["status_code"]) < 400
|
||||
return data
|
||||
|
||||
def raise_for_status(self):
|
||||
"""Raise the appropriate exception if the request was not successful."""
|
||||
if not self.is_successful:
|
||||
if self.status_code in (401, 403):
|
||||
raise OnspringAuthenticationError(self.message or "Authentication failed")
|
||||
if self.status_code == 404:
|
||||
raise OnspringNotFoundError(self.message or "Resource not found")
|
||||
if self.status_code == 429:
|
||||
raise OnspringRateLimitError(self.message or "Rate limit exceeded")
|
||||
raise OnspringError(self.message or f"Request failed with status {self.status_code}")
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Pydantic models for Onspring field API responses."""
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ListValue(BaseModel):
|
||||
"""A selectable value within a list field definition."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
sort_order: int = Field(alias="sortOrder")
|
||||
numeric_value: Optional[int] = Field(default=None, alias="numericValue")
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
class OnspringField(BaseModel):
|
||||
"""Represents an Onspring field definition."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
id: int
|
||||
app_id: int = Field(alias="appId")
|
||||
name: str
|
||||
type: str
|
||||
status: str
|
||||
is_required: bool = Field(alias="isRequired")
|
||||
is_unique: bool = Field(alias="isUnique")
|
||||
list_id: Optional[int] = Field(default=None, alias="listId")
|
||||
values: Optional[list[ListValue]] = None
|
||||
multiplicity: Optional[str] = None
|
||||
output_type: Optional[str] = Field(default=None, alias="outputType")
|
||||
|
||||
|
||||
class GetFieldByIdResponse(BaseModel):
|
||||
"""Response containing a single field."""
|
||||
|
||||
field: OnspringField
|
||||
|
||||
|
||||
class GetFieldsByIdsResponse(BaseModel):
|
||||
"""Response containing fields for requested IDs."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
count: int
|
||||
fields: list[OnspringField] = Field(alias="items")
|
||||
|
||||
|
||||
class GetFieldsByAppIdResponse(BaseModel):
|
||||
"""Paginated response containing fields for an app."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page_number: int = Field(alias="pageNumber")
|
||||
page_size: int = Field(alias="pageSize")
|
||||
total_pages: int = Field(alias="totalPages")
|
||||
total_records: int = Field(alias="totalRecords")
|
||||
fields: list[OnspringField] = Field(alias="items")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Pydantic models for Onspring file API requests and responses."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class File(BaseModel):
|
||||
"""Represents a file with its metadata and binary content."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
name: str
|
||||
content_type: str = Field(alias="contentType")
|
||||
content_length: int = Field(alias="contentLength")
|
||||
content: bytes
|
||||
|
||||
|
||||
class FileInfo(BaseModel):
|
||||
"""Metadata about a file stored in Onspring."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
type: str
|
||||
content_type: str = Field(alias="contentType")
|
||||
name: str
|
||||
created_date: Optional[datetime] = Field(default=None, alias="createdDate")
|
||||
modified_date: Optional[datetime] = Field(default=None, alias="modifiedDate")
|
||||
owner: str
|
||||
file_href: str = Field(alias="fileHref")
|
||||
|
||||
|
||||
class GetFileInfoByIdResponse(BaseModel):
|
||||
"""Response containing file metadata."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
file_info: FileInfo = Field(alias="fileInfo")
|
||||
|
||||
|
||||
class GetFileByIdResponse(BaseModel):
|
||||
"""Response containing a file with binary content."""
|
||||
|
||||
file: File
|
||||
|
||||
|
||||
class SaveFileRequest(BaseModel):
|
||||
"""Request payload for uploading a file to a record field."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
record_id: int = Field(alias="recordId")
|
||||
field_id: int = Field(alias="fieldId")
|
||||
file_name: str = Field(alias="fileName")
|
||||
file_path: str = Field(alias="filePath")
|
||||
content_type: str = Field(alias="contentType")
|
||||
notes: Optional[str] = None
|
||||
modified_date: Optional[datetime] = Field(default=None, alias="modifiedDate")
|
||||
|
||||
|
||||
class SaveFileResponse(BaseModel):
|
||||
"""Response containing the ID of a saved file."""
|
||||
|
||||
id: int
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Pydantic models for Onspring list item API requests and responses."""
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ListItemRequest(BaseModel):
|
||||
"""Request payload for adding or updating a list item."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
list_id: int = Field(alias="listId")
|
||||
name: str
|
||||
id: Optional[uuid.UUID] = None
|
||||
numeric_value: Optional[int] = Field(default=None, alias="numericValue")
|
||||
color: Optional[str] = None
|
||||
|
||||
|
||||
class AddOrUpdateListItemResponse(BaseModel):
|
||||
"""Response containing the ID of an added or updated list item."""
|
||||
|
||||
id: uuid.UUID
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Pydantic models for Onspring record data, requests, and responses."""
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, Tag
|
||||
|
||||
from onspring_api_sdk.enums import DataFormat
|
||||
|
||||
|
||||
class RecordFieldValue(BaseModel):
|
||||
"""Base model for all record field value types."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
field_id: int = Field(alias="fieldId")
|
||||
value: object = None
|
||||
type: str
|
||||
|
||||
|
||||
class StringFieldValue(RecordFieldValue):
|
||||
"""Field value containing a string."""
|
||||
|
||||
type: Literal["String"] = "String"
|
||||
value: Optional[str] = None
|
||||
|
||||
|
||||
class IntegerFieldValue(RecordFieldValue):
|
||||
"""Field value containing an integer."""
|
||||
|
||||
type: Literal["Integer"] = "Integer"
|
||||
value: Optional[int] = None
|
||||
|
||||
|
||||
class DecimalFieldValue(RecordFieldValue):
|
||||
"""Field value containing a decimal number."""
|
||||
|
||||
type: Literal["Decimal"] = "Decimal"
|
||||
value: Optional[Decimal] = None
|
||||
|
||||
|
||||
class DateFieldValue(RecordFieldValue):
|
||||
"""Field value containing a date."""
|
||||
|
||||
type: Literal["Date"] = "Date"
|
||||
value: Optional[datetime] = None
|
||||
|
||||
|
||||
class GuidFieldValue(RecordFieldValue):
|
||||
"""Field value containing a GUID."""
|
||||
|
||||
type: Literal["Guid"] = "Guid"
|
||||
value: Optional[uuid.UUID] = None
|
||||
|
||||
|
||||
class TimeSpanData(BaseModel):
|
||||
"""Time span configuration with increment, recurrence, and end conditions."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
quantity: Optional[Decimal] = None
|
||||
increment: Optional[str] = None
|
||||
recurrence: Optional[str] = None
|
||||
end_by_date: Optional[datetime] = Field(default=None, alias="endByDate")
|
||||
end_after_occurrences: Optional[int] = Field(default=None, alias="endAfterOccurrences")
|
||||
|
||||
|
||||
class TimeSpanValue(RecordFieldValue):
|
||||
"""Field value containing a time span."""
|
||||
|
||||
type: Literal["TimeSpan"] = "TimeSpan"
|
||||
value: Optional[TimeSpanData] = None
|
||||
|
||||
|
||||
class StringListValue(RecordFieldValue):
|
||||
"""Field value containing a list of strings."""
|
||||
|
||||
type: Literal["StringList"] = "StringList"
|
||||
value: Optional[list[str]] = None
|
||||
|
||||
|
||||
class IntegerListValue(RecordFieldValue):
|
||||
"""Field value containing a list of integers."""
|
||||
|
||||
type: Literal["IntegerList"] = "IntegerList"
|
||||
value: Optional[list[int]] = None
|
||||
|
||||
|
||||
class GuidListValue(RecordFieldValue):
|
||||
"""Field value containing a list of GUIDs."""
|
||||
|
||||
type: Literal["GuidList"] = "GuidList"
|
||||
value: Optional[list[uuid.UUID]] = None
|
||||
|
||||
|
||||
class Attachment(BaseModel):
|
||||
"""An attachment associated with a record field."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
file_id: int = Field(alias="fileId")
|
||||
file_name: str = Field(alias="fileName")
|
||||
notes: Optional[str] = None
|
||||
storage_location: str = Field(alias="storageLocation")
|
||||
|
||||
|
||||
class AttachmentListValue(RecordFieldValue):
|
||||
"""Field value containing a list of attachments."""
|
||||
|
||||
type: Literal["AttachmentList"] = "AttachmentList"
|
||||
value: Optional[list[Attachment]] = None
|
||||
|
||||
|
||||
class ScoringGroup(BaseModel):
|
||||
"""A scoring group with a list value reference, name, and scores."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
list_value_id: Optional[uuid.UUID] = Field(default=None, alias="listValueId")
|
||||
name: Optional[str] = None
|
||||
score: Optional[Decimal] = None
|
||||
maximum_score: Optional[Decimal] = Field(default=None, alias="maximumScore")
|
||||
delegate_type: Optional[str] = Field(default=None, alias="delegateType")
|
||||
|
||||
|
||||
class ScoringGroupListValue(RecordFieldValue):
|
||||
"""Field value containing a list of scoring groups."""
|
||||
|
||||
type: Literal["ScoringGroupList"] = "ScoringGroupList"
|
||||
value: Optional[list[ScoringGroup]] = None
|
||||
|
||||
|
||||
class FileListValue(RecordFieldValue):
|
||||
"""Field value containing a list of file IDs."""
|
||||
|
||||
type: Literal["FileList"] = "FileList"
|
||||
value: Optional[list[int]] = None
|
||||
|
||||
|
||||
FieldValue = Annotated[
|
||||
Union[
|
||||
Annotated[StringFieldValue, Tag("String")],
|
||||
Annotated[IntegerFieldValue, Tag("Integer")],
|
||||
Annotated[DecimalFieldValue, Tag("Decimal")],
|
||||
Annotated[DateFieldValue, Tag("Date")],
|
||||
Annotated[GuidFieldValue, Tag("Guid")],
|
||||
Annotated[TimeSpanValue, Tag("TimeSpan")],
|
||||
Annotated[StringListValue, Tag("StringList")],
|
||||
Annotated[IntegerListValue, Tag("IntegerList")],
|
||||
Annotated[GuidListValue, Tag("GuidList")],
|
||||
Annotated[AttachmentListValue, Tag("AttachmentList")],
|
||||
Annotated[ScoringGroupListValue, Tag("ScoringGroupList")],
|
||||
Annotated[FileListValue, Tag("FileList")],
|
||||
],
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
class Record(BaseModel):
|
||||
"""A record containing typed field values for a specific app."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
record_id: Optional[int] = Field(default=None, alias="recordId")
|
||||
fields: list[FieldValue] = Field(default_factory=list, alias="fieldData")
|
||||
|
||||
|
||||
class GetRecordsByAppRequest(BaseModel):
|
||||
"""Request parameters for fetching records by app ID."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
field_ids: list[int] = Field(default_factory=list, alias="fieldIds")
|
||||
data_format: str = Field(default=DataFormat.Raw.name, alias="dataFormat")
|
||||
page_number: int = Field(default=1, alias="pageNumber")
|
||||
page_size: int = Field(default=50, alias="pageSize")
|
||||
|
||||
|
||||
class QueryRecordsRequest(BaseModel):
|
||||
"""Request parameters for querying records with a filter."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
filter: str
|
||||
field_ids: list[int] = Field(default_factory=list, alias="fieldIds")
|
||||
data_format: str = Field(default=DataFormat.Raw.name, alias="dataFormat")
|
||||
page_number: int = Field(default=1, alias="pageNumber")
|
||||
page_size: int = Field(default=50, alias="pageSize")
|
||||
|
||||
|
||||
class GetRecordByIdRequest(BaseModel):
|
||||
"""Request parameters for fetching a record by ID."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
record_id: int = Field(alias="recordId")
|
||||
field_ids: list[int] = Field(default_factory=list, alias="fieldIds")
|
||||
data_format: str = Field(default=DataFormat.Raw.name, alias="dataFormat")
|
||||
|
||||
|
||||
class GetBatchRecordsRequest(BaseModel):
|
||||
"""Request parameters for fetching multiple records by IDs."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
record_ids: list[int] = Field(alias="recordIds")
|
||||
field_ids: list[int] = Field(default_factory=list, alias="fieldIds")
|
||||
data_format: str = Field(default=DataFormat.Raw.name, alias="dataFormat")
|
||||
|
||||
|
||||
class DeleteBatchRecordsRequest(BaseModel):
|
||||
"""Request payload for deleting multiple records."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
record_ids: list[int] = Field(alias="recordIds")
|
||||
|
||||
|
||||
class GetRecordsResponse(BaseModel):
|
||||
"""Paginated response containing records."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page_number: int = Field(alias="pageNumber")
|
||||
page_size: int = Field(alias="pageSize")
|
||||
total_pages: int = Field(alias="totalPages")
|
||||
total_records: int = Field(alias="totalRecords")
|
||||
records: list[Record] = Field(alias="items")
|
||||
|
||||
|
||||
class GetBatchRecordsResponse(BaseModel):
|
||||
"""Response containing records for requested IDs."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
count: int
|
||||
records: list[Record] = Field(alias="items")
|
||||
|
||||
|
||||
class AddOrUpdateRecordResponse(BaseModel):
|
||||
"""Response containing the ID and warnings from an add/update operation."""
|
||||
|
||||
id: int
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Pydantic models for Onspring report API requests and responses."""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from onspring_api_sdk.enums import DataFormat, ReportDataType
|
||||
|
||||
|
||||
class Row(BaseModel):
|
||||
"""A single row of report data."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
record_id: Optional[int] = Field(alias="recordId", default=None)
|
||||
cells: list[Any]
|
||||
|
||||
|
||||
class Report(BaseModel):
|
||||
"""Represents an Onspring report."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
app_id: int = Field(alias="appId")
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class GetReportByIdRequest(BaseModel):
|
||||
"""Request parameters for fetching a report by ID."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
report_id: int = Field(alias="reportId")
|
||||
api_data_format: str = Field(default=DataFormat.Raw.name, alias="apiDataFormat")
|
||||
data_type: str = Field(default=ReportDataType.ReportData.name, alias="dataType")
|
||||
|
||||
|
||||
class GetReportByIdResponse(BaseModel):
|
||||
"""Response containing report columns and rows."""
|
||||
|
||||
columns: list[str]
|
||||
rows: list[Row]
|
||||
|
||||
|
||||
class GetReportsByAppIdResponse(BaseModel):
|
||||
"""Paginated response containing reports for an app."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
page_number: int = Field(alias="pageNumber")
|
||||
page_size: int = Field(alias="pageSize")
|
||||
total_pages: int = Field(alias="totalPages")
|
||||
total_records: int = Field(alias="totalRecords")
|
||||
reports: list[Report] = Field(alias="items")
|
||||
Reference in New Issue
Block a user