Skip to content

Commit c41a246

Browse files
committed
Migrate to uv, drop 3.9 and 3.10, fix tests
1 parent 0571f93 commit c41a246

13 files changed

Lines changed: 1075 additions & 1483 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,66 +20,87 @@ jobs:
2020
test_unit:
2121
strategy:
2222
matrix:
23-
python-version: [ "3.9","3.10","3.11", "3.12" ]
23+
python-version: [ "3.11", "3.12", "3.13" ]
2424
runs-on: ubuntu-latest
2525
steps:
2626
- uses: actions/checkout@v4
27+
- uses: astral-sh/setup-uv@v6
2728
- name: Set up Python ${{ matrix.python-version }}
2829
uses: actions/setup-python@v5
2930
with:
3031
python-version: ${{ matrix.python-version }}
3132
- name: Install dependencies
33+
env:
34+
UV_PYTHON: ${{ matrix.python-version }}
3235
run: make install
3336
- name: Run unit tests
37+
env:
38+
UV_PYTHON: ${{ matrix.python-version }}
3439
run: |
3540
make test-unit
3641
3742
lint:
3843
runs-on: ubuntu-latest
3944
steps:
4045
- uses: actions/checkout@v4
46+
- uses: astral-sh/setup-uv@v6
47+
- name: Set up Python 3.13
48+
uses: actions/setup-python@v5
49+
with:
50+
python-version: "3.13"
4151
- name: Install dependencies
52+
env:
53+
UV_PYTHON: "3.13"
4254
run: make install
4355
- name: Lint
56+
env:
57+
UV_PYTHON: "3.13"
4458
run: |
4559
make lint
4660
4761
test_integration:
4862
strategy:
4963
matrix:
50-
python-version: [ "3.9","3.10","3.11", "3.12" ]
64+
python-version: [ "3.11", "3.12", "3.13" ]
5165
runs-on: ubuntu-latest
5266
steps:
5367
- uses: actions/checkout@v4
68+
- uses: astral-sh/setup-uv@v6
5469
- name: Set up Python ${{ matrix.python-version }}
5570
uses: actions/setup-python@v5
5671
with:
5772
python-version: ${{ matrix.python-version }}
5873
- name: Install dependencies
74+
env:
75+
UV_PYTHON: ${{ matrix.python-version }}
5976
run: make install
6077
- name: Run integration tests
61-
run: |
62-
make test-integration-docker
6378
env:
79+
UV_PYTHON: ${{ matrix.python-version }}
6480
UNSTRUCTURED_API_KEY: ${{ secrets.UNSTRUCTURED_API_KEY }}
81+
run: |
82+
make test-integration-docker
6583
6684
test_contract:
6785
strategy:
6886
matrix:
69-
python-version: [ "3.9","3.10","3.11", "3.12" ]
87+
python-version: [ "3.11", "3.12", "3.13" ]
7088
runs-on: ubuntu-latest
71-
env:
72-
POETRY_VIRTUALENVS_IN_PROJECT: "true"
7389
steps:
7490
- uses: actions/checkout@v4
91+
- uses: astral-sh/setup-uv@v6
7592
- name: Set up Python ${{ matrix.python-version }}
7693
uses: actions/setup-python@v5
7794
with:
7895
python-version: ${{ matrix.python-version }}
7996
- name: Install dependencies
97+
env:
98+
UV_PYTHON: ${{ matrix.python-version }}
8099
run: |
81100
make install
82101
- name: Run contract tests
102+
env:
103+
UV_PYTHON: ${{ matrix.python-version }}
83104
run: |
84105
make test-contract
85106

Makefile

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ DOCKER_IMAGE ?= downloads.unstructured.io/unstructured-io/unstructured-api:lates
1010
## install: installs all test, dev, and experimental requirements
1111
.PHONY: install
1212
install:
13-
pip install -U poetry
1413
python scripts/prepare_readme.py
15-
poetry install
14+
uv sync
1615

1716
## install-speakeasy-cli: download the speakeasy cli tool
1817
.PHONY: install-speakeasy-cli
@@ -28,30 +27,30 @@ test: test-unit test-integration-docker
2827

2928
.PHONY: test-unit
3029
test-unit:
31-
PYTHONPATH=. poetry run pytest -n auto _test_unstructured_client -v -k "unit"
30+
PYTHONPATH=. uv run pytest -n auto _test_unstructured_client -v -k "unit"
3231

3332
.PHONY: test-contract
3433
test-contract:
35-
PYTHONPATH=. poetry run pytest -n auto _test_contract -v
34+
PYTHONPATH=. uv run pytest -n auto _test_contract -v
3635

3736
# Assumes you have unstructured-api running on localhost:8000
3837
.PHONY: test-integration
3938
test-integration:
40-
PYTHONPATH=. poetry run pytest -n auto _test_unstructured_client -v -k "integration"
39+
PYTHONPATH=. uv run pytest -n auto _test_unstructured_client -v -k "integration"
4140

4241
# Runs the unstructured-api in docker for tests
4342
.PHONY: test-integration-docker
4443
test-integration-docker:
4544
-docker stop unstructured-api && docker kill unstructured-api
4645
docker run --name unstructured-api -p 8000:8000 -d --rm ${DOCKER_IMAGE} --host 0.0.0.0 && \
4746
curl -s -o /dev/null --retry 10 --retry-delay 5 --retry-all-errors http://localhost:8000/general/docs && \
48-
PYTHONPATH=. poetry run pytest -n auto _test_unstructured_client -v -k "integration" && \
47+
PYTHONPATH=. uv run pytest -n auto _test_unstructured_client -v -k "integration" && \
4948
docker kill unstructured-api
5049

5150
.PHONY: lint
5251
lint:
53-
poetry run pylint --rcfile=pylintrc src
54-
poetry run mypy src
52+
uv run pylint --rcfile=pylintrc src
53+
uv run mypy src
5554

5655
#############
5756
# Speakeasy #

_test_contract/conftest.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
from unstructured_client import UnstructuredClient, utils
99

10-
# Python 3.9 workaround: eagerly import retries to avoid lazy import race condition
11-
# This prevents a KeyError in module lock when templates.py triggers lazy import of utils.retries
10+
# Eagerly import retries to avoid a lazy import race when templates.py first loads utils.retries.
1211
from unstructured_client.utils import retries # noqa: F401
1312

1413
FAKE_API_KEY = "91pmLBeETAbXCpNylRsLq11FdiZPTk"

_test_unstructured_client/integration/test_integration.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@
1313
from unstructured_client.utils.retries import BackoffStrategy, RetryConfig
1414

1515

16+
FAKE_KEY = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
17+
LOCAL_API_URL = "http://localhost:8000"
18+
19+
1620
@pytest.fixture(scope="function")
1721
def client() -> UnstructuredClient:
18-
_client = UnstructuredClient(api_key_auth=os.getenv("UNSTRUCTURED_API_KEY"))
22+
_client = UnstructuredClient(
23+
api_key_auth=os.getenv("UNSTRUCTURED_API_KEY") or FAKE_KEY,
24+
server_url=os.getenv("UNSTRUCTURED_SERVER_URL") or LOCAL_API_URL,
25+
timeout_ms=120_000,
26+
)
1927
yield _client
2028

2129

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from pathlib import Path
2+
import tomllib
3+
4+
from unstructured_client.models import shared
5+
from unstructured_client.utils.forms import serialize_multipart_form
6+
7+
8+
REPO_ROOT = Path(__file__).resolve().parents[2]
9+
10+
11+
def _load_pyproject() -> dict:
12+
return tomllib.loads((REPO_ROOT / "pyproject.toml").read_text())
13+
14+
15+
def test_pyproject_invariants():
16+
data = _load_pyproject()
17+
project = data["project"]
18+
19+
assert project["dynamic"] == ["version"]
20+
assert "version" not in project
21+
assert project["requires-python"] == ">=3.11"
22+
assert "httpcore >=1.0.9" in project["dependencies"]
23+
assert "pydantic >=2.12.5" in project["dependencies"]
24+
assert not any("cryptography" in d for d in project["dependencies"]), \
25+
"cryptography is unused and must not be a runtime dependency"
26+
27+
dynamic_version = data["tool"]["setuptools"]["dynamic"]["version"]
28+
assert dynamic_version == {"attr": "unstructured_client._version.__version__"}
29+
30+
build = data["build-system"]
31+
assert build["build-backend"] == "setuptools.build_meta"
32+
assert "setuptools>=80" in build["requires"]
33+
34+
35+
def test_publish_script_is_hardened():
36+
publish_script = (REPO_ROOT / "scripts" / "publish.sh").read_text()
37+
38+
assert "set -euo pipefail" in publish_script
39+
assert "sys.version_info < (3, 11)" in publish_script
40+
assert 'uv publish --token "${PYPI_TOKEN}" --check-url https://pypi.org/simple' in publish_script
41+
42+
43+
def test_body_create_job_input_files_are_serialized_as_multipart_files():
44+
request = shared.BodyCreateJob(
45+
request_data="{}",
46+
input_files=[
47+
shared.InputFiles(
48+
content=b"hello",
49+
file_name="hello.pdf",
50+
content_type="application/pdf",
51+
)
52+
],
53+
)
54+
55+
media_type, form, files = serialize_multipart_form("multipart/form-data", request)
56+
57+
assert media_type == "multipart/form-data"
58+
assert form == {"request_data": "{}"}
59+
assert files == [("input_files[]", ("hello.pdf", b"hello", "application/pdf"))]
60+
61+
62+
def test_body_run_workflow_input_files_are_serialized_as_multipart_files():
63+
request = shared.BodyRunWorkflow(
64+
input_files=[
65+
shared.BodyRunWorkflowInputFiles(
66+
content=b"hello",
67+
file_name="hello.pdf",
68+
content_type="application/pdf",
69+
)
70+
]
71+
)
72+
73+
media_type, form, files = serialize_multipart_form("multipart/form-data", request)
74+
75+
assert media_type == "multipart/form-data"
76+
assert form == {}
77+
assert files == [("input_files[]", ("hello.pdf", b"hello", "application/pdf"))]

gen.yaml

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,39 @@ generation:
1111
securityFeb2025: false
1212
sharedErrorComponentsApr2025: false
1313
sharedNestedComponentsJan2026: false
14+
nameOverrideFeb2026: false
1415
auth:
1516
oAuth2ClientCredentialsEnabled: false
1617
oAuth2PasswordEnabled: false
18+
hoistGlobalSecurity: true
19+
schemas:
20+
allOfMergeStrategy: shallowMerge
21+
requestBodyFieldName: ""
22+
versioningStrategy: automatic
23+
persistentEdits:
24+
enabled: true
1725
tests:
1826
generateTests: true
1927
generateNewTests: false
2028
skipResponseBodyAssertions: false
21-
persistentEdits: {}
22-
requestBodyFieldName: ""
23-
schemas:
24-
allOfMergeStrategy: shallowMerge
2529
python:
26-
version: 0.42.12
30+
version: 0.43.0
2731
additionalDependencies:
2832
dev:
29-
deepdiff: '>=6.0'
30-
freezegun: '>=1.5.1'
31-
pytest: '>=8.3.3'
32-
pytest-asyncio: =1.1.0
33+
deepdiff: '>=9.0.0'
34+
freezegun: '>=1.5.5'
35+
pytest: '>=8.4.2'
36+
pytest-asyncio: '>=1.3.0'
3337
pytest-httpx: '>=0.35.0'
34-
pytest-mock: '>=3.14.0'
35-
pytest-xdist: ^3.5.0
36-
types-aiofiles: '>=24.1.0'
37-
uvloop: '>=0.20.0'
38+
pytest-mock: '>=3.15.1'
39+
pytest-xdist: '>=3.8.0'
40+
types-aiofiles: '>=25.1.0.20251011'
41+
uvloop: '>=0.22.1'
3842
main:
39-
aiofiles: '>=24.1.0'
40-
cryptography: '>=3.1'
41-
httpx: '>=0.27.0'
42-
pypdf: '>= 6.2.0'
43-
pypdfium2: '>= 5.0.0'
43+
aiofiles: '>=25.1.0'
44+
httpx: '>=0.28.1'
45+
pypdf: '>=6.9.2'
46+
pypdfium2: '>=5.6.0'
4447
requests-toolbelt: '>=1.0.0'
4548
allowedRedefinedBuiltins:
4649
- id
@@ -56,10 +59,14 @@ python:
5659
enableCustomCodeRegions: true
5760
enumFormat: enum
5861
fixFlags:
62+
asyncPaginationSep2025: false
63+
conflictResistantModelImportsFeb2026: false
5964
responseRequiredSep2024: false
6065
flattenGlobalSecurity: true
6166
flattenRequests: false
6267
flatteningOrder: parameters-first
68+
forwardCompatibleEnumsByDefault: false
69+
forwardCompatibleUnionsByDefault: "false"
6370
imports:
6471
option: openapi
6572
paths:
@@ -76,7 +83,7 @@ python:
7683
moduleName: ""
7784
multipartArrayFormat: legacy
7885
outputModelSuffix: output
79-
packageManager: poetry
86+
packageManager: uv
8087
packageName: unstructured-client
8188
preApplyUnionDiscriminators: false
8289
projectUrls: {}

0 commit comments

Comments
 (0)