Skip to content

Commit cbdc576

Browse files
committed
feat: add fragment options
1 parent 11015c5 commit cbdc576

File tree

5 files changed

+1001
-818
lines changed

5 files changed

+1001
-818
lines changed

rath/rath.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from graphql import (
1616
DocumentNode,
1717
)
18-
from rath.operation import GraphQLResult, opify
18+
from rath.operation import GraphQLResult, Operation, opify
1919
from contextvars import ContextVar, Token
2020
from koil import unkoil_gen, unkoil
2121

@@ -88,6 +88,22 @@ def validate_link(
8888

8989
return link
9090

91+
async def aquery_operation(self, operation: Operation) -> GraphQLResult:
92+
"""Asynchronously executes a query or mutation using the Rath client."""
93+
result = None
94+
95+
async for data in self.link.aexecute(operation):
96+
result = data
97+
break
98+
99+
if not result:
100+
raise NotConnectedError("Could not retrieve data from the server.")
101+
# This is to account for the fact that mypy apparently doesn't
102+
# understand that a return statement in a generator is valid.
103+
# This is a workaround to make mypy happy.
104+
105+
return result
106+
91107
async def aquery(
92108
self,
93109
query: Union[str, DocumentNode],
@@ -117,19 +133,7 @@ async def aquery(
117133
"""
118134
op = opify(query, variables, headers, operation_name, **kwargs)
119135

120-
result = None
121-
122-
async for data in self.link.aexecute(op):
123-
result = data
124-
break
125-
126-
if not result:
127-
raise NotConnectedError("Could not retrieve data from the server.")
128-
# This is to account for the fact that mypy apparently doesn't
129-
# understand that a return statement in a generator is valid.
130-
# This is a workaround to make mypy happy.
131-
132-
return result
136+
return await self.aquery_operation(op)
133137

134138
def query(
135139
self,

rath/scalars.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@ class WithId(Protocol):
4747
id: "IDCoercible"
4848

4949

50-
IDCoercible = str | ID | WithId | int
50+
IDCoercible = int | str | ID | WithId
5151
""" A type that can be coerced into an ID."""

rath/traits.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Any, TypeVar, Type
2+
from rath import Rath
3+
from rath.scalars import IDCoercible
4+
from rath.turms.fragment import fetch_fragment_via, TurmsFragment, afetch_fragment_via
5+
from rath.turms.utils import get_attributes_or_error
6+
7+
T = TypeVar("T", bound=TurmsFragment[Any])
8+
9+
10+
class FederationFetchable:
11+
@classmethod
12+
def get_identifier(cls: Type[T]) -> str:
13+
"""Get the identifier for the fragment."""
14+
return cls.Meta.type
15+
16+
@classmethod
17+
def get_rath(cls: Type[T]) -> "Rath":
18+
"""Get the current Rath client from the context."""
19+
raise NotImplementedError(
20+
"This method should be implemented by the subclass to return the current Rath client."
21+
)
22+
23+
@classmethod
24+
def expand(cls: Type[T], id: IDCoercible) -> T:
25+
"""Fetch an entity by its ID using the current Rath client."""
26+
return fetch_fragment_via(
27+
cls,
28+
id=id,
29+
rath=cls.get_rath(),
30+
)
31+
32+
@classmethod
33+
async def aexpand(cls: Type[T], id: IDCoercible) -> T:
34+
"""Asynchronously fetch an entity by its ID using the current Rath client."""
35+
return await afetch_fragment_via(
36+
cls,
37+
id=id,
38+
rath=cls.get_rath(),
39+
)
40+
41+
async def ashrink(self) -> None:
42+
"""Asynchronously shrink the entity."""
43+
return get_attributes_or_error(self, "id")
44+
45+
def shrink(self) -> None:
46+
"""Shrink the entity."""
47+
return get_attributes_or_error(self, "id")

rath/turms/fragment.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
from rath.rath import Rath, current_rath
2+
from typing import AsyncIterator, Iterator, Protocol, Type, TypeVar, Any, Dict, Optional
3+
from pydantic import BaseModel
4+
5+
from rath.scalars import IDCoercible
6+
7+
8+
# --- Base meta description ---
9+
class TurmsMeta(Protocol):
10+
"""Meta class for Turms operations"""
11+
12+
document: str
13+
name: str
14+
type: str
15+
16+
17+
TMeta = TypeVar("TMeta", bound=TurmsMeta)
18+
19+
20+
class TurmsFragment(Protocol[TMeta]):
21+
"""Represents a Turms operation that is both callable and its own return type."""
22+
23+
Meta: Type[TMeta]
24+
25+
def __init__(self, *args: Any, **kwargs: Any) -> None:
26+
"""A pydantic constructor"""
27+
...
28+
29+
30+
TFragment = TypeVar("TFragment", bound=TurmsFragment[Any])
31+
32+
33+
def fetch_fragment_via(
34+
fragment: Type[TFragment],
35+
id: IDCoercible,
36+
rath: Optional[Rath] = None,
37+
) -> TFragment:
38+
"""Synchronously Executes an a query or mutation using rath
39+
40+
41+
This function will execute an operation using rath, retrieving
42+
the currentliy active rath client from the current_rath context
43+
if no rath client is provided.
44+
45+
Parameters
46+
----------
47+
operation : TurmsOperation
48+
The turms operation to execute
49+
variables : Dict[str, Any]
50+
The variables to use
51+
rath : Optional[Rath], optional
52+
The rath client, by default the current rath client
53+
54+
Returns
55+
-------
56+
BaseModel
57+
The result of the operation
58+
"""
59+
rath = rath or current_rath.get()
60+
assert rath is not None, (
61+
"No rath client provided and no rath client in current_rath context"
62+
)
63+
return fragment(
64+
**rath.query(
65+
"""
66+
%s
67+
query Entities($representations: [_Any!]!) {
68+
entities: _entities(representations: $representations) {
69+
...%s
70+
}
71+
}
72+
"""
73+
% (
74+
fragment.Meta.document,
75+
fragment.Meta.name,
76+
),
77+
{"representations": [{"__typename": fragment.Meta.type, "id": id}]},
78+
).data["entities"][0],
79+
)
80+
81+
82+
async def afetch_fragment_via(
83+
fragment: Type[TFragment],
84+
id: IDCoercible,
85+
rath: Optional[Rath] = None,
86+
) -> TFragment:
87+
"""Synchronously Executes an a query or mutation using rath
88+
89+
90+
This function will execute an operation using rath, retrieving
91+
the currentliy active rath client from the current_rath context
92+
if no rath client is provided.
93+
94+
Parameters
95+
----------
96+
operation : TurmsOperation
97+
The turms operation to execute
98+
variables : Dict[str, Any]
99+
The variables to use
100+
rath : Optional[Rath], optional
101+
The rath client, by default the current rath client
102+
103+
Returns
104+
-------
105+
BaseModel
106+
The result of the operation
107+
"""
108+
rath = rath or current_rath.get()
109+
assert rath is not None, (
110+
"No rath client provided and no rath client in current_rath context"
111+
)
112+
return fragment(
113+
**(
114+
await rath.aquery(
115+
"""
116+
%s
117+
query Entities($representations: [_Any!]!) {
118+
entities: _entities(representations: $representations) {
119+
...%s
120+
}
121+
}
122+
"""
123+
% (
124+
fragment.Meta.document,
125+
fragment.Meta.name,
126+
),
127+
{"representations": [{"__typename": fragment.Meta.name, "id": id}]},
128+
)
129+
).data["entities"][0],
130+
)

0 commit comments

Comments
 (0)