Skip to content

Commit e863b44

Browse files
authored
Merge pull request #118 from tharropoulos/async
feat: add async support
2 parents e8e8e2b + 604d910 commit e863b44

File tree

159 files changed

+11627
-3629
lines changed

Some content is hidden

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

159 files changed

+11627
-3629
lines changed

.github/workflows/test-and-lint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ jobs:
4444
- name: Install the project
4545
run: uv sync --locked --all-extras --dev
4646

47+
- name: Check sync generation
48+
run: uv run python utils/run-unasync.py --check
49+
4750
- name: Lint with Ruff
4851
run: |
4952
uv run ruff check src/typesense

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,38 @@ You can find some examples [here](https://github.com/typesense/typesense-python/
1616

1717
See detailed [API documentation](https://typesense.org/api).
1818

19+
## Async usage
20+
21+
Use `AsyncClient` when working in an async runtime:
22+
23+
```python
24+
import asyncio
25+
import typesense
26+
27+
28+
async def main() -> None:
29+
client = typesense.AsyncClient({
30+
"api_key": "abcd",
31+
"nodes": [{"host": "localhost", "port": "8108", "protocol": "http"}],
32+
"connection_timeout_seconds": 2,
33+
})
34+
35+
print(await client.collections.retrieve())
36+
await client.api_call.aclose()
37+
38+
39+
if __name__ == "__main__":
40+
asyncio.run(main())
41+
```
42+
43+
See `examples/async_collection_operations.py` for a fuller async walkthrough.
44+
1945
## Compatibility
2046

2147
| Typesense Server | typesense-python |
2248
|------------------|------------------|
49+
| \>= v30.0 | \>= v2.0.0 |
50+
| \>= v28.0 | \>= v1.0.0 |
2351
| \>= v26.0 | \>= v0.20.0 |
2452
| \>= v0.25.0 | \>= v0.16.0 |
2553
| \>= v0.23.0 | \>= v0.14.0 |
@@ -32,7 +60,11 @@ See detailed [API documentation](https://typesense.org/api).
3260

3361
## Contributing
3462

63+
> [!NOTE]
64+
> Development happens in async-only code; sync code is generated automatically via `utils/run-unasync.py`.
65+
3566
Bug reports and pull requests are welcome on GitHub at [https://github.com/typesense/typesense-python].
67+
If you change any part of the client's source code, run `uv run utils/run-unasync.py` before opening a PR to keep the generated sync files in sync.
3668

3769
## License
3870

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import asyncio
2+
import json
3+
import os
4+
import sys
5+
6+
curr_dir = os.path.dirname(os.path.realpath(__file__))
7+
repo_root = os.path.abspath(os.path.join(curr_dir, os.pardir))
8+
sys.path.insert(1, os.path.join(repo_root, "src"))
9+
10+
import typesense
11+
from typesense.exceptions import TypesenseClientError
12+
13+
14+
async def main() -> None:
15+
client = typesense.AsyncClient(
16+
{
17+
"api_key": "xyz",
18+
"nodes": [
19+
{
20+
"host": "localhost",
21+
"port": "8108",
22+
"protocol": "http",
23+
}
24+
],
25+
"connection_timeout_seconds": 2,
26+
}
27+
)
28+
29+
try:
30+
# Drop pre-existing collection if any
31+
try:
32+
await client.collections["books"].delete()
33+
except Exception:
34+
pass
35+
36+
# Create a collection
37+
create_response = await client.collections.create(
38+
{
39+
"name": "books",
40+
"fields": [
41+
{"name": "title", "type": "string"},
42+
{"name": "authors", "type": "string[]", "facet": True},
43+
{"name": "publication_year", "type": "int32", "facet": True},
44+
{"name": "ratings_count", "type": "int32"},
45+
{"name": "average_rating", "type": "float"},
46+
{"name": "image_url", "type": "string"},
47+
],
48+
"default_sorting_field": "ratings_count",
49+
}
50+
)
51+
52+
print(create_response)
53+
54+
# Retrieve the collection we just created
55+
retrieve_response = await client.collections["books"].retrieve()
56+
print(retrieve_response)
57+
58+
# Try retrieving all collections
59+
retrieve_all_response = await client.collections.retrieve()
60+
print(retrieve_all_response)
61+
62+
# Add a book
63+
hunger_games_book = {
64+
"id": "1",
65+
"authors": ["Suzanne Collins"],
66+
"average_rating": 4.34,
67+
"publication_year": 2008,
68+
"title": "The Hunger Games",
69+
"image_url": "https://images.gr-assets.com/books/1447303603m/2767052.jpg",
70+
"ratings_count": 4780653,
71+
}
72+
73+
await client.collections["books"].documents.create(hunger_games_book)
74+
75+
# Upsert the same document
76+
print(await client.collections["books"].documents.upsert(hunger_games_book))
77+
78+
# Or update it
79+
hunger_games_book_updated = {"id": "1", "average_rating": 4.45}
80+
print(
81+
await client.collections["books"]
82+
.documents["1"]
83+
.update(hunger_games_book_updated)
84+
)
85+
86+
# Try updating with bad data (with coercion enabled)
87+
hunger_games_book_updated = {"id": "1", "average_rating": "4.55"}
88+
print(
89+
await client.collections["books"]
90+
.documents["1"]
91+
.update(hunger_games_book_updated, {"dirty_values": "coerce_or_reject"})
92+
)
93+
94+
# Export the documents from a collection
95+
export_output = await client.collections["books"].documents.export()
96+
print(export_output)
97+
98+
# Fetch a document in a collection
99+
print(await client.collections["books"].documents["1"].retrieve())
100+
101+
# Search for documents in a collection
102+
print(
103+
await client.collections["books"].documents.search(
104+
{
105+
"q": "hunger",
106+
"query_by": "title",
107+
"sort_by": "ratings_count:desc",
108+
}
109+
)
110+
)
111+
112+
# Make multiple search requests at the same time
113+
print(
114+
await client.multi_search.perform(
115+
{
116+
"searches": [
117+
{
118+
"q": "hunger",
119+
"query_by": "title",
120+
},
121+
{
122+
"q": "suzanne",
123+
"query_by": "authors",
124+
},
125+
]
126+
},
127+
{"collection": "books", "sort_by": "ratings_count:desc"},
128+
)
129+
)
130+
131+
# Remove a document from a collection
132+
print(await client.collections["books"].documents["1"].delete())
133+
134+
# Import documents into a collection
135+
docs_to_import = []
136+
for exported_doc_str in export_output.split("\n"):
137+
docs_to_import.append(json.loads(exported_doc_str))
138+
139+
import_results = await client.collections["books"].documents.import_(
140+
docs_to_import
141+
)
142+
print(import_results)
143+
144+
# Upserting documents
145+
import_results = await client.collections["books"].documents.import_(
146+
docs_to_import,
147+
{
148+
"action": "upsert",
149+
"return_id": True,
150+
},
151+
)
152+
print(import_results)
153+
154+
# Schema change: add optional field
155+
schema_change = {
156+
"fields": [{"name": "in_stock", "optional": True, "type": "bool"}]
157+
}
158+
print(await client.collections["books"].update(schema_change))
159+
160+
# Update value matching a filter
161+
updated_doc = {"publication_year": 2009}
162+
print(
163+
await client.collections["books"].documents.update(
164+
updated_doc, {"filter_by": "publication_year: 2008"}
165+
)
166+
)
167+
168+
# Drop the field
169+
schema_change = {"fields": [{"name": "in_stock", "drop": True}]}
170+
print(await client.collections["books"].update(schema_change))
171+
172+
# Deleting documents matching a filter query
173+
print(
174+
await client.collections["books"].documents.delete(
175+
{"filter_by": "ratings_count: 4780653"}
176+
)
177+
)
178+
179+
# Try importing empty list
180+
try:
181+
import_results = await client.collections["books"].documents.import_(
182+
[], {"action": "upsert"}
183+
)
184+
print(import_results)
185+
except TypesenseClientError:
186+
print("Detected import of empty document list.")
187+
188+
# Drop the collection
189+
drop_response = await client.collections["books"].delete()
190+
print(drop_response)
191+
finally:
192+
await client.api_call.aclose()
193+
194+
195+
if __name__ == "__main__":
196+
asyncio.run(main())

pyproject.toml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ classifiers = [
2121
"Programming Language :: Python :: 3.12",
2222
"Programming Language :: Python :: 3.13",
2323
]
24-
dependencies = ["requests", "typing-extensions"]
24+
dependencies = [
25+
"httpx>=0.28.1",
26+
"typing-extensions",
27+
]
2528
dynamic = ["version"]
2629

2730
[project.urls]
@@ -35,16 +38,18 @@ build-backend = "setuptools.build_meta"
3538

3639
[dependency-groups]
3740
dev = [
38-
"mypy",
41+
"mypy>=1.19.0",
3942
"pytest",
43+
"pytest-asyncio",
4044
"coverage",
4145
"pytest-mock",
42-
"requests-mock",
4346
"python-dotenv",
44-
"types-requests",
4547
"faker",
4648
"ruff>=0.11.11",
4749
"isort>=6.0.1",
50+
"respx>=0.22.0",
51+
"requests",
52+
"unasync>=0.6.0",
4853
]
4954

5055
[tool.uv]

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[pytest]
22
pythonpath = src
3+
asyncio_mode = auto
34
markers =
45
open_ai

src/typesense/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .client import Client # NOQA
1+
from .sync.client import Client # NOQA
2+
from .async_.client import AsyncClient # NOQA
23

34

4-
__all__ = ["Client"]
5-
__version__ = "1.3.0"
5+
__version__ = "2.0.0"
6+
__all__ = ["Client", "AsyncClient"]

src/typesense/alias.py

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

0 commit comments

Comments
 (0)