Skip to content

Commit 6ceae34

Browse files
Update Python SDK to correctly support Pydantic v1 and v2 (#89)
Co-authored-by: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Co-authored-by: Rohan Konnur <[email protected]>
1 parent cce82ff commit 6ceae34

File tree

784 files changed

+51440
-52183
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

784 files changed

+51440
-52183
lines changed

.fernignore

-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
README.md
33
CODEOWNERS
44
LICENSE.md
5-
tests/test_client.py
65

76
# We need to configure secrets for integration tests.
87
.github/workflows/ci.yml

poetry.lock

+184-131
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,55 @@
11
[tool.poetry]
22
name = "MergePythonClient"
3-
version = "1.0.9"
3+
version = "1.0.10"
44
description = ""
55
readme = "README.md"
66
authors = []
7+
keywords = []
8+
9+
classifiers = [
10+
"Intended Audience :: Developers",
11+
"Programming Language :: Python",
12+
"Programming Language :: Python :: 3",
13+
"Programming Language :: Python :: 3.8",
14+
"Programming Language :: Python :: 3.9",
15+
"Programming Language :: Python :: 3.10",
16+
"Programming Language :: Python :: 3.11",
17+
"Programming Language :: Python :: 3.12",
18+
"Operating System :: OS Independent",
19+
"Operating System :: POSIX",
20+
"Operating System :: MacOS",
21+
"Operating System :: POSIX :: Linux",
22+
"Operating System :: Microsoft :: Windows",
23+
"Topic :: Software Development :: Libraries :: Python Modules",
24+
"Typing :: Typed"
25+
]
726
packages = [
827
{ include = "merge", from = "src"}
928
]
1029

30+
[project.urls]
31+
Repository = 'https://github.com/merge-api/merge-python-client'
32+
1133
[tool.poetry.dependencies]
1234
python = "^3.8"
1335
httpx = ">=0.21.2"
1436
pydantic = ">= 1.9.2"
1537
typing_extensions = ">= 4.0.0"
1638

1739
[tool.poetry.dev-dependencies]
18-
mypy = "^1.8.0"
40+
mypy = "1.0.1"
1941
pytest = "^7.4.0"
42+
pytest-asyncio = "^0.23.5"
43+
python-dateutil = "^2.9.0"
44+
types-python-dateutil = "^2.9.0.20240316"
45+
46+
[tool.pytest.ini_options]
47+
testpaths = [ "tests" ]
48+
asyncio_mode = "auto"
49+
50+
[tool.mypy]
51+
plugins = ["pydantic.mypy"]
52+
2053

2154
[build-system]
2255
requires = ["poetry-core"]

src/merge/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
from .resources import accounting, ats, crm, filestorage, hris, ticketing
44
from .environment import MergeEnvironment
5+
from .version import __version__
56

6-
__all__ = ["MergeEnvironment", "accounting", "ats", "crm", "filestorage", "hris", "ticketing"]
7+
__all__ = ["MergeEnvironment", "__version__", "accounting", "ats", "crm", "filestorage", "hris", "ticketing"]

src/merge/client.py

+96-6
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,142 @@
1515

1616

1717
class Merge:
18+
"""
19+
Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions.
20+
21+
Parameters
22+
----------
23+
base_url : typing.Optional[str]
24+
The base url to use for requests from the client.
25+
26+
environment : MergeEnvironment
27+
The environment to use for requests from the client. from .environment import MergeEnvironment
28+
29+
30+
31+
Defaults to MergeEnvironment.PRODUCTION
32+
33+
34+
35+
account_token : typing.Optional[str]
36+
api_key : typing.Union[str, typing.Callable[[], str]]
37+
timeout : typing.Optional[float]
38+
The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced.
39+
40+
follow_redirects : typing.Optional[bool]
41+
Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in.
42+
43+
httpx_client : typing.Optional[httpx.Client]
44+
The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration.
45+
46+
Examples
47+
--------
48+
from merge.client import Merge
49+
50+
client = Merge(
51+
account_token="YOUR_ACCOUNT_TOKEN",
52+
api_key="YOUR_API_KEY",
53+
)
54+
"""
55+
1856
def __init__(
1957
self,
2058
*,
2159
base_url: typing.Optional[str] = None,
2260
environment: MergeEnvironment = MergeEnvironment.PRODUCTION,
2361
account_token: typing.Optional[str] = None,
2462
api_key: typing.Union[str, typing.Callable[[], str]],
25-
timeout: typing.Optional[float] = 60,
63+
timeout: typing.Optional[float] = None,
64+
follow_redirects: typing.Optional[bool] = True,
2665
httpx_client: typing.Optional[httpx.Client] = None
2766
):
67+
_defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None
2868
self._client_wrapper = SyncClientWrapper(
2969
base_url=_get_base_url(base_url=base_url, environment=environment),
3070
account_token=account_token,
3171
api_key=api_key,
32-
httpx_client=httpx.Client(timeout=timeout) if httpx_client is None else httpx_client,
72+
httpx_client=httpx_client
73+
if httpx_client is not None
74+
else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects)
75+
if follow_redirects is not None
76+
else httpx.Client(timeout=_defaulted_timeout),
77+
timeout=_defaulted_timeout,
3378
)
3479
self.ats = AtsClient(client_wrapper=self._client_wrapper)
3580
self.crm = CrmClient(client_wrapper=self._client_wrapper)
3681
self.filestorage = FilestorageClient(client_wrapper=self._client_wrapper)
37-
self.ticketing = TicketingClient(client_wrapper=self._client_wrapper)
3882
self.hris = HrisClient(client_wrapper=self._client_wrapper)
83+
self.ticketing = TicketingClient(client_wrapper=self._client_wrapper)
3984
self.accounting = AccountingClient(client_wrapper=self._client_wrapper)
4085

4186

4287
class AsyncMerge:
88+
"""
89+
Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions.
90+
91+
Parameters
92+
----------
93+
base_url : typing.Optional[str]
94+
The base url to use for requests from the client.
95+
96+
environment : MergeEnvironment
97+
The environment to use for requests from the client. from .environment import MergeEnvironment
98+
99+
100+
101+
Defaults to MergeEnvironment.PRODUCTION
102+
103+
104+
105+
account_token : typing.Optional[str]
106+
api_key : typing.Union[str, typing.Callable[[], str]]
107+
timeout : typing.Optional[float]
108+
The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced.
109+
110+
follow_redirects : typing.Optional[bool]
111+
Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in.
112+
113+
httpx_client : typing.Optional[httpx.AsyncClient]
114+
The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration.
115+
116+
Examples
117+
--------
118+
from merge.client import AsyncMerge
119+
120+
client = AsyncMerge(
121+
account_token="YOUR_ACCOUNT_TOKEN",
122+
api_key="YOUR_API_KEY",
123+
)
124+
"""
125+
43126
def __init__(
44127
self,
45128
*,
46129
base_url: typing.Optional[str] = None,
47130
environment: MergeEnvironment = MergeEnvironment.PRODUCTION,
48131
account_token: typing.Optional[str] = None,
49132
api_key: typing.Union[str, typing.Callable[[], str]],
50-
timeout: typing.Optional[float] = 60,
133+
timeout: typing.Optional[float] = None,
134+
follow_redirects: typing.Optional[bool] = True,
51135
httpx_client: typing.Optional[httpx.AsyncClient] = None
52136
):
137+
_defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None
53138
self._client_wrapper = AsyncClientWrapper(
54139
base_url=_get_base_url(base_url=base_url, environment=environment),
55140
account_token=account_token,
56141
api_key=api_key,
57-
httpx_client=httpx.AsyncClient(timeout=timeout) if httpx_client is None else httpx_client,
142+
httpx_client=httpx_client
143+
if httpx_client is not None
144+
else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects)
145+
if follow_redirects is not None
146+
else httpx.AsyncClient(timeout=_defaulted_timeout),
147+
timeout=_defaulted_timeout,
58148
)
59149
self.ats = AsyncAtsClient(client_wrapper=self._client_wrapper)
60150
self.crm = AsyncCrmClient(client_wrapper=self._client_wrapper)
61151
self.filestorage = AsyncFilestorageClient(client_wrapper=self._client_wrapper)
62-
self.ticketing = AsyncTicketingClient(client_wrapper=self._client_wrapper)
63152
self.hris = AsyncHrisClient(client_wrapper=self._client_wrapper)
153+
self.ticketing = AsyncTicketingClient(client_wrapper=self._client_wrapper)
64154
self.accounting = AsyncAccountingClient(client_wrapper=self._client_wrapper)
65155

66156

src/merge/core/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@
33
from .api_error import ApiError
44
from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper
55
from .datetime_utils import serialize_datetime
6+
from .file import File, convert_file_dict_to_httpx_tuples
7+
from .http_client import AsyncHttpClient, HttpClient
68
from .jsonable_encoder import jsonable_encoder
9+
from .pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1
10+
from .query_encoder import encode_query
711
from .remove_none_from_dict import remove_none_from_dict
812
from .request_options import RequestOptions
913

1014
__all__ = [
1115
"ApiError",
1216
"AsyncClientWrapper",
17+
"AsyncHttpClient",
1318
"BaseClientWrapper",
19+
"File",
20+
"HttpClient",
1421
"RequestOptions",
1522
"SyncClientWrapper",
23+
"convert_file_dict_to_httpx_tuples",
24+
"deep_union_pydantic_dicts",
25+
"encode_query",
1626
"jsonable_encoder",
27+
"pydantic_v1",
1728
"remove_none_from_dict",
1829
"serialize_datetime",
1930
]

src/merge/core/client_wrapper.py

+24-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import httpx
66

7+
from .http_client import AsyncHttpClient, HttpClient
8+
79

810
class BaseClientWrapper:
911
def __init__(
@@ -12,16 +14,18 @@ def __init__(
1214
account_token: typing.Optional[str] = None,
1315
api_key: typing.Union[str, typing.Callable[[], str]],
1416
base_url: str,
17+
timeout: typing.Optional[float] = None,
1518
):
1619
self._account_token = account_token
1720
self._api_key = api_key
1821
self._base_url = base_url
22+
self._timeout = timeout
1923

2024
def get_headers(self) -> typing.Dict[str, str]:
2125
headers: typing.Dict[str, str] = {
2226
"X-Fern-Language": "Python",
2327
"X-Fern-SDK-Name": "MergePythonClient",
24-
"X-Fern-SDK-Version": "1.0.9",
28+
"X-Fern-SDK-Version": "1.0.10",
2529
}
2630
if self._account_token is not None:
2731
headers["X-Account-Token"] = self._account_token
@@ -37,6 +41,9 @@ def _get_api_key(self) -> str:
3741
def get_base_url(self) -> str:
3842
return self._base_url
3943

44+
def get_timeout(self) -> typing.Optional[float]:
45+
return self._timeout
46+
4047

4148
class SyncClientWrapper(BaseClientWrapper):
4249
def __init__(
@@ -45,10 +52,16 @@ def __init__(
4552
account_token: typing.Optional[str] = None,
4653
api_key: typing.Union[str, typing.Callable[[], str]],
4754
base_url: str,
55+
timeout: typing.Optional[float] = None,
4856
httpx_client: httpx.Client,
4957
):
50-
super().__init__(account_token=account_token, api_key=api_key, base_url=base_url)
51-
self.httpx_client = httpx_client
58+
super().__init__(account_token=account_token, api_key=api_key, base_url=base_url, timeout=timeout)
59+
self.httpx_client = HttpClient(
60+
httpx_client=httpx_client,
61+
base_headers=self.get_headers(),
62+
base_timeout=self.get_timeout(),
63+
base_url=self.get_base_url(),
64+
)
5265

5366

5467
class AsyncClientWrapper(BaseClientWrapper):
@@ -58,7 +71,13 @@ def __init__(
5871
account_token: typing.Optional[str] = None,
5972
api_key: typing.Union[str, typing.Callable[[], str]],
6073
base_url: str,
74+
timeout: typing.Optional[float] = None,
6175
httpx_client: httpx.AsyncClient,
6276
):
63-
super().__init__(account_token=account_token, api_key=api_key, base_url=base_url)
64-
self.httpx_client = httpx_client
77+
super().__init__(account_token=account_token, api_key=api_key, base_url=base_url, timeout=timeout)
78+
self.httpx_client = AsyncHttpClient(
79+
httpx_client=httpx_client,
80+
base_headers=self.get_headers(),
81+
base_timeout=self.get_timeout(),
82+
base_url=self.get_base_url(),
83+
)

src/merge/core/file.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This file was auto-generated by Fern from our API Definition.
2+
3+
import typing
4+
5+
# File typing inspired by the flexibility of types within the httpx library
6+
# https://github.com/encode/httpx/blob/master/httpx/_types.py
7+
FileContent = typing.Union[typing.IO[bytes], bytes, str]
8+
File = typing.Union[
9+
# file (or bytes)
10+
FileContent,
11+
# (filename, file (or bytes))
12+
typing.Tuple[typing.Optional[str], FileContent],
13+
# (filename, file (or bytes), content_type)
14+
typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]],
15+
# (filename, file (or bytes), content_type, headers)
16+
typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str], typing.Mapping[str, str]],
17+
]
18+
19+
20+
def convert_file_dict_to_httpx_tuples(
21+
d: typing.Dict[str, typing.Union[File, typing.List[File]]]
22+
) -> typing.List[typing.Tuple[str, File]]:
23+
"""
24+
The format we use is a list of tuples, where the first element is the
25+
name of the file and the second is the file object. Typically HTTPX wants
26+
a dict, but to be able to send lists of files, you have to use the list
27+
approach (which also works for non-lists)
28+
https://github.com/encode/httpx/pull/1032
29+
"""
30+
31+
httpx_tuples = []
32+
for key, file_like in d.items():
33+
if isinstance(file_like, list):
34+
for file_like_item in file_like:
35+
httpx_tuples.append((key, file_like_item))
36+
else:
37+
httpx_tuples.append((key, file_like))
38+
return httpx_tuples

0 commit comments

Comments
 (0)