Skip to content

Commit aaa2104

Browse files
authored
Make it really sans-io (#3)
1 parent 1ece2fa commit aaa2104

111 files changed

Lines changed: 2992 additions & 4207 deletions

File tree

Some content is hidden

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

‎.github/dependabot.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: uv
5+
directory: /
6+
schedule:
7+
interval: monthly
8+
groups:
9+
# One pull request for all of them: each pull request costs a CI run and a review.
10+
all:
11+
patterns: ["*"]
12+
13+
- package-ecosystem: github-actions
14+
directory: /
15+
schedule:
16+
interval: weekly

‎.vscode/extensions.json‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"recommendations": [
3+
"astral-sh.ty",
4+
"charliermarsh.ruff",
5+
"graphql.vscode-graphql-syntax"
6+
]
7+
}

‎.vscode/settings.json‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
{
2+
"[python]": {
3+
"editor.codeActionsOnSave": {
4+
"source.fixAll": "explicit",
5+
"source.organizeImports": "explicit"
6+
},
7+
"editor.defaultFormatter": "charliermarsh.ruff",
8+
"editor.formatOnType": true
9+
},
10+
"debugpy.debugJustMyCode": false,
11+
"files.insertFinalNewline": true,
12+
"files.readonlyInclude": {
13+
"bookshop/*_graphql.py": true,
14+
"bookshop/graphql/**": true,
15+
"src/graphql_codegen/_cli/_introspection_graphql.py": true
16+
},
17+
"files.trimFinalNewlines": true,
18+
"files.trimTrailingWhitespace": true,
19+
"python.languageServer": "None",
220
"python.testing.pytestArgs": [
321
"."
422
],
23+
"python.testing.pytestEnabled": true,
524
"python.testing.unittestEnabled": false,
6-
"python.testing.pytestEnabled": true
25+
"search.exclude": {
26+
"bookshop/*_graphql.py": true,
27+
"bookshop/graphql/**": true,
28+
"src/graphql_codegen/_cli/_introspection_graphql.py": true
29+
}
730
}

‎README.md‎

Lines changed: 223 additions & 194 deletions
Large diffs are not rendered by default.

‎bookshop/__main__.py‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
from uuid import uuid4
2-
31
from bookshop.app import describe
4-
from bookshop.client.injection import injectors
52
from bookshop.scalar import ISBN
6-
from bookshop.transport import Client, transport
73

8-
client = Client(
9-
transport,
10-
injectors=injectors({"idempotencyKey": uuid4}),
11-
)
12-
print(describe(ISBN("9780141439518"), client=client))
4+
print(describe(ISBN("9780141439518")))

‎bookshop/app.py‎

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,20 @@
1818
Search,
1919
SearchData,
2020
)
21-
from bookshop.client.runtime import ExecutionError, UnexpectedNullError
22-
from bookshop.client.schema import Address, BookFilter, OrderStatus
2321
from bookshop.get_order_graphql import GetOrder, GetOrderData
22+
from bookshop.graphql.runtime import ExecutionError, UnexpectedNullError, merge
23+
from bookshop.graphql.schema import Address, BookFilter, OrderStatus
2424
from bookshop.scalar import ISBN
25-
from bookshop.transport import Client, SubscriptionClient
25+
from bookshop.transport import send, subscribe
2626

2727

28-
def describe(isbn: ISBN, /, *, client: Client) -> str:
28+
def describe(isbn: ISBN, /) -> str:
2929
variables: GetBookVariables = {"lookup": {"isbn": isbn}}
3030

31+
# Caught only to show what the error carries.
32+
# You would usually let the error propagate since a null here is a bug.
3133
try:
32-
data = client(GetBook(variables))
34+
data = send(GetBook(variables))
3335
except UnexpectedNullError as error:
3436
assert error.path == ["book"]
3537
assert error.__notes__ == [f"Raised by `GetBook` with variables {variables!r}."]
@@ -46,15 +48,10 @@ def describe(isbn: ISBN, /, *, client: Client) -> str:
4648
return f"{book['title']}, {by}, costs {book['price']:.2f}."
4749

4850

49-
def book_and_similar(
50-
isbn: ISBN, text: str, /, *, client: Client
51-
) -> tuple[str, list[str]]:
52-
# Two queries in one call to the transport.
53-
book_data, search_data = client(
54-
(
55-
GetBook({"lookup": {"isbn": isbn}}),
56-
Search({"text": text}),
57-
)
51+
def book_and_similar(isbn: ISBN, text: str, /) -> tuple[str, list[str]]:
52+
# Two queries in one request.
53+
book_data, search_data = send(
54+
merge(GetBook({"lookup": {"isbn": isbn}}), Search({"text": text}))
5855
)
5956
assert_type(book_data, GetBookData)
6057
assert_type(search_data, SearchData)
@@ -67,19 +64,21 @@ def book_and_similar(
6764

6865

6966
def look_up(
70-
isbns: Sequence[ISBN], publication_ids: Sequence[str], /, *, client: Client
67+
isbns: Sequence[ISBN], publication_ids: Sequence[str], /
7168
) -> tuple[GetBookData | GetPublicationData, ...]:
72-
# Any number of queries in one call to the transport.
73-
return client( # ty: ignore[unsound-return-statement] # Pyright and Pyrefly already infer this.
74-
[
75-
*(GetBook({"lookup": {"isbn": isbn}}) for isbn in isbns),
76-
*(GetPublication({"id": id_}) for id_ in publication_ids),
77-
]
69+
# Any number of queries in one request.
70+
return send( # ty: ignore[unsound-return-statement] # Pyright and Pyrefly already infer this.
71+
merge(
72+
[
73+
*(GetBook({"lookup": {"isbn": isbn}}) for isbn in isbns),
74+
*(GetPublication({"id": id_}) for id_ in publication_ids),
75+
]
76+
)
7877
)
7978

8079

81-
def run_saved_search(name: str, /, *, client: Client) -> list[str]:
82-
data = client(GetSavedSearch({"name": name}))
80+
def run_saved_search(name: str, /) -> list[str]:
81+
data = send(GetSavedSearch({"name": name}))
8382
search = data["savedSearch"]
8483

8584
if search is None:
@@ -88,12 +87,12 @@ def run_saved_search(name: str, /, *, client: Client) -> list[str]:
8887
book_filter = search["value"]
8988
assert_type(book_filter, BookFilter | None)
9089
# Sent back as is.
91-
books = client(ListBooks({"filter": book_filter}))
90+
books = send(ListBooks({"filter": book_filter}))
9291
return [book["title"] for book in books["books"]]
9392

9493

95-
def length(publication_id: str, /, *, client: Client) -> str:
96-
data = client(GetPublication({"id": publication_id}))
94+
def length(publication_id: str, /) -> str:
95+
data = send(GetPublication({"id": publication_id}))
9796
publication = data["publication"]
9897

9998
if publication is None:
@@ -120,12 +119,12 @@ def status_label(status: OrderStatus, /) -> str:
120119
case "CANCELED":
121120
return "Canceled"
122121
case _:
123-
# A member added after this client was generated.
122+
# A member added after codegen.
124123
return status.replace("_", " ").capitalize()
125124

126125

127-
def cheaper_than(limit: Decimal, /, *, client: Client) -> list[str]:
128-
data = client(ListBooks({"filter": {"priceBelow": limit}}))
126+
def cheaper_than(limit: Decimal, /) -> list[str]:
127+
data = send(ListBooks({"filter": {"priceBelow": limit}}))
129128
labels: list[str] = []
130129

131130
for book in data["books"]:
@@ -136,8 +135,8 @@ def cheaper_than(limit: Decimal, /, *, client: Client) -> list[str]:
136135
return labels
137136

138137

139-
def order(book_id: str, address: Address, /, *, client: Client) -> str:
140-
data = client(
138+
def order(book_id: str, address: Address, /) -> str:
139+
data = send(
141140
PlaceOrder(
142141
{"input": {"lines": [{"book": book_id}], "shippingAddress": address}}
143142
)
@@ -148,24 +147,8 @@ def order(book_id: str, address: Address, /, *, client: Client) -> str:
148147
return f"Order {order['id']}: {order['total']:.2f} at {order['placedAt']:%H:%M}."
149148

150149

151-
def cancel(order_ids: list[str], /, *, client: Client) -> list[str]:
152-
"""Cancel the orders in one call to the transport, and explain each failure."""
153-
results = client(
154-
[
155-
CancelOrder({"input": {"order": order_id}}).returning_error()
156-
for order_id in order_ids
157-
]
158-
)
159-
# Each error has a note naming its operation and the variables sent.
160-
return [
161-
f"{error.__notes__[0]} {error!s}"
162-
for error in results
163-
if isinstance(error, ExecutionError)
164-
]
165-
166-
167-
def track(order_id: str, /, *, client: Client) -> str:
168-
result = client(GetOrder({"id": order_id}).returning_error())
150+
def track(order_id: str, /) -> str:
151+
result = send(GetOrder({"id": order_id}).returning_error())
169152

170153
if isinstance(result, ExecutionError):
171154
data = result.parse_data()
@@ -180,12 +163,27 @@ def track(order_id: str, /, *, client: Client) -> str:
180163
)
181164

182165

183-
def watch(order_id: str, /, *, client: SubscriptionClient) -> list[OrderStatus]:
184-
"""Follow the order until it is delivered, and return its statuses."""
166+
def cancel(order_ids: list[str], /) -> list[str]:
167+
results = send(
168+
merge(
169+
[
170+
CancelOrder({"input": {"order": order_id}}).returning_error()
171+
for order_id in order_ids
172+
]
173+
)
174+
)
175+
# Each error has a note naming its operation and the variables sent.
176+
return [
177+
f"{error.__notes__[0]} {error!s}"
178+
for error in results
179+
if isinstance(error, ExecutionError)
180+
]
181+
182+
183+
def watch(order_id: str, /) -> list[OrderStatus]:
185184
statuses: list[OrderStatus] = []
186-
events = client(OnOrderStatusChanged({"orderId": order_id}))
185+
events = subscribe(OnOrderStatusChanged({"orderId": order_id}))
187186

188-
# Closing the stream, however the loop ends, unsubscribes.
189187
with closing(events):
190188
for event in events:
191189
statuses.append(event["orderStatusChanged"]["status"])

0 commit comments

Comments
 (0)