Skip to content

Commit cc42365

Browse files
chore: go live (#250)
1 parent 7b057fc commit cc42365

15 files changed

+124
-80
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
33

44
USER vscode
55

6-
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash
6+
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
77
ENV PATH=/home/vscode/.rye/shims:$PATH
88

99
RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
curl -sSf https://rye.astral.sh/get | bash
2222
echo "$HOME/.rye/shims" >> $GITHUB_PATH
2323
env:
24-
RYE_VERSION: 0.24.0
24+
RYE_VERSION: '0.35.0'
2525
RYE_INSTALL_OPTION: '--yes'
2626

2727
- name: Install dependencies
@@ -41,7 +41,7 @@ jobs:
4141
curl -sSf https://rye.astral.sh/get | bash
4242
echo "$HOME/.rye/shims" >> $GITHUB_PATH
4343
env:
44-
RYE_VERSION: 0.24.0
44+
RYE_VERSION: '0.35.0'
4545
RYE_INSTALL_OPTION: '--yes'
4646

4747
- name: Bootstrap

.github/workflows/create-releases.yml

Lines changed: 0 additions & 38 deletions
This file was deleted.

.github/workflows/handle-release-pr-title-edit.yml

Lines changed: 0 additions & 25 deletions
This file was deleted.

.github/workflows/publish-pypi.yml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
# workflow for re-running publishing to PyPI in case it fails for some reason
2-
# you can run this workflow by navigating to https://www.github.com/intercom/python-intercom/actions/workflows/publish-pypi.yml
1+
# This workflow is triggered when a GitHub release is created.
2+
# It can also be run manually to re-publish to PyPI in case it failed for some reason.
3+
# You can run this workflow by navigating to https://www.github.com/intercom/python-intercom/actions/workflows/publish-pypi.yml
34
name: Publish PyPI
45
on:
56
workflow_dispatch:
67

8+
release:
9+
types: [published]
10+
711
jobs:
812
publish:
913
name: publish
@@ -17,8 +21,8 @@ jobs:
1721
curl -sSf https://rye.astral.sh/get | bash
1822
echo "$HOME/.rye/shims" >> $GITHUB_PATH
1923
env:
20-
RYE_VERSION: 0.24.0
21-
RYE_INSTALL_OPTION: "--yes"
24+
RYE_VERSION: '0.35.0'
25+
RYE_INSTALL_OPTION: '--yes'
2226

2327
- name: Publish to PyPI
2428
run: |

.github/workflows/release-doctor.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ jobs:
1616
run: |
1717
bash ./bin/check-release-environment
1818
env:
19-
STAINLESS_API_KEY: ${{ secrets.STAINLESS_API_KEY }}
2019
PYPI_TOKEN: ${{ secrets.INTERCOM_PYPI_TOKEN || secrets.PYPI_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.prism.log
12
.vscode
23
_dev
34

bin/check-release-environment

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
errors=()
44

5-
if [ -z "${STAINLESS_API_KEY}" ]; then
6-
errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.")
7-
fi
8-
95
if [ -z "${PYPI_TOKEN}" ]; then
106
errors+=("The INTERCOM_PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.")
117
fi

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dev-dependencies = [
5858
"nox",
5959
"dirty-equals>=0.6.0",
6060
"importlib-metadata>=6.7.0",
61+
"rich>=13.7.1",
6162

6263
]
6364

@@ -99,6 +100,21 @@ include = [
99100
[tool.hatch.build.targets.wheel]
100101
packages = ["src/python_minus_intercom"]
101102

103+
[tool.hatch.build.targets.sdist]
104+
# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc)
105+
include = [
106+
"/*.toml",
107+
"/*.json",
108+
"/*.lock",
109+
"/*.md",
110+
"/mypy.ini",
111+
"/noxfile.py",
112+
"bin/*",
113+
"examples/*",
114+
"src/*",
115+
"tests/*",
116+
]
117+
102118
[tool.hatch.metadata.hooks.fancy-pypi-readme]
103119
content-type = "text/markdown"
104120

requirements-dev.lock

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
# features: []
77
# all-features: true
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
annotated-types==0.6.0
1213
# via pydantic
13-
anyio==4.1.0
14+
anyio==4.4.0
1415
# via httpx
1516
# via python-intercom
1617
argcomplete==3.1.2
@@ -44,6 +45,10 @@ idna==3.4
4445
importlib-metadata==7.0.0
4546
iniconfig==2.0.0
4647
# via pytest
48+
markdown-it-py==3.0.0
49+
# via rich
50+
mdurl==0.1.2
51+
# via markdown-it-py
4752
mypy==1.7.1
4853
mypy-extensions==1.0.0
4954
# via mypy
@@ -63,6 +68,8 @@ pydantic==2.7.1
6368
# via python-intercom
6469
pydantic-core==2.18.2
6570
# via pydantic
71+
pygments==2.18.0
72+
# via rich
6673
pyright==1.1.364
6774
pytest==7.1.1
6875
# via pytest-asyncio
@@ -72,6 +79,7 @@ python-dateutil==2.8.2
7279
pytz==2023.3.post1
7380
# via dirty-equals
7481
respx==0.20.2
82+
rich==13.7.1
7583
ruff==0.1.9
7684
setuptools==68.2.2
7785
# via nodeenv
@@ -86,6 +94,7 @@ tomli==2.0.1
8694
# via mypy
8795
# via pytest
8896
typing-extensions==4.8.0
97+
# via anyio
8998
# via mypy
9099
# via pydantic
91100
# via pydantic-core

requirements.lock

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
# features: []
77
# all-features: true
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
annotated-types==0.6.0
1213
# via pydantic
13-
anyio==4.1.0
14+
anyio==4.4.0
1415
# via httpx
1516
# via python-intercom
1617
certifi==2023.7.22
@@ -38,6 +39,7 @@ sniffio==1.3.0
3839
# via httpx
3940
# via python-intercom
4041
typing-extensions==4.8.0
42+
# via anyio
4143
# via pydantic
4244
# via pydantic-core
4345
# via python-intercom

src/python_minus_intercom/_base_client.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
HttpxSendArgs,
5959
AsyncTransport,
6060
RequestOptions,
61+
HttpxRequestFiles,
6162
ModelBuilderProtocol,
6263
)
6364
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
@@ -459,6 +460,7 @@ def _build_request(
459460
headers = self._build_headers(options)
460461
params = _merge_mappings(self.default_query, options.params)
461462
content_type = headers.get("Content-Type")
463+
files = options.files
462464

463465
# If the given Content-Type header is multipart/form-data then it
464466
# has to be removed so that httpx can generate the header with
@@ -472,14 +474,23 @@ def _build_request(
472474
headers.pop("Content-Type")
473475

474476
# As we are now sending multipart/form-data instead of application/json
475-
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding
477+
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
476478
if json_data:
477479
if not is_dict(json_data):
478480
raise TypeError(
479481
f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead."
480482
)
481483
kwargs["data"] = self._serialize_multipartform(json_data)
482484

485+
# httpx determines whether or not to send a "multipart/form-data"
486+
# request based on the truthiness of the "files" argument.
487+
# This gets around that issue by generating a dict value that
488+
# evaluates to true.
489+
#
490+
# https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186
491+
if not files:
492+
files = cast(HttpxRequestFiles, ForceMultipartDict())
493+
483494
# TODO: report this error to httpx
484495
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
485496
headers=headers,
@@ -492,7 +503,7 @@ def _build_request(
492503
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
493504
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
494505
json=json_data,
495-
files=options.files,
506+
files=files,
496507
**kwargs,
497508
)
498509

@@ -1863,6 +1874,11 @@ def make_request_options(
18631874
return options
18641875

18651876

1877+
class ForceMultipartDict(Dict[str, None]):
1878+
def __bool__(self) -> bool:
1879+
return True
1880+
1881+
18661882
class OtherPlatform:
18671883
def __init__(self, name: str) -> None:
18681884
self.name = name

src/python_minus_intercom/_models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ClassVar,
1111
Protocol,
1212
Required,
13+
ParamSpec,
1314
TypedDict,
1415
TypeGuard,
1516
final,
@@ -67,6 +68,9 @@
6768
__all__ = ["BaseModel", "GenericModel"]
6869

6970
_T = TypeVar("_T")
71+
_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
72+
73+
P = ParamSpec("P")
7074

7175

7276
@runtime_checkable
@@ -379,6 +383,29 @@ def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericMo
379383
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
380384

381385

386+
def build(
387+
base_model_cls: Callable[P, _BaseModelT],
388+
*args: P.args,
389+
**kwargs: P.kwargs,
390+
) -> _BaseModelT:
391+
"""Construct a BaseModel class without validation.
392+
393+
This is useful for cases where you need to instantiate a `BaseModel`
394+
from an API response as this provides type-safe params which isn't supported
395+
by helpers like `construct_type()`.
396+
397+
```py
398+
build(MyModel, my_field_a="foo", my_field_b=123)
399+
```
400+
"""
401+
if args:
402+
raise TypeError(
403+
"Received positional arguments which are not supported; Keyword arguments must be used instead",
404+
)
405+
406+
return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
407+
408+
382409
def construct_type(*, value: object, type_: object) -> object:
383410
"""Loose coercion to the expected type with construction of nested values.
384411

src/python_minus_intercom/_utils/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,7 @@
4949
maybe_transform as maybe_transform,
5050
async_maybe_transform as async_maybe_transform,
5151
)
52-
from ._reflection import function_has_argument as function_has_argument
52+
from ._reflection import (
53+
function_has_argument as function_has_argument,
54+
assert_signatures_in_sync as assert_signatures_in_sync,
55+
)

0 commit comments

Comments
 (0)