fragment-python 1.0.0__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fragment_python-1.0.0 → fragment_python-1.1.0}/PKG-INFO +33 -4
- {fragment_python-1.0.0 → fragment_python-1.1.0}/README.md +31 -2
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/client/async_client.py +8 -5
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/client/sync_client.py +8 -5
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/codegen/helpers.py +7 -0
- fragment_python-1.1.0/fragment/codegen/main.py +98 -0
- fragment_python-1.1.0/fragment/codegen/plugins/generate_typed_entries.py +204 -0
- fragment_python-1.1.0/fragment/codegen/typed_entries.py +524 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/exceptions.py +2 -2
- fragment_python-1.1.0/fragment/py.typed +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/__init__.py +35 -0
- fragment_python-1.1.0/fragment/sdk/add_ledger_entries.py +84 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/async_client.py +8 -5
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/client.py +66 -1
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/enums.py +6 -0
- {fragment_python-1.0.0/fragment/sync_sdk → fragment_python-1.1.0/fragment/sdk}/input_types.py +42 -16
- fragment_python-1.1.0/fragment/sdk/typed_entries.py +95 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/std_queries/queries.graphql +46 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/__init__.py +35 -0
- fragment_python-1.1.0/fragment/sync_sdk/add_ledger_entries.py +84 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/async_client.py +8 -5
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/client.py +66 -1
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/enums.py +6 -0
- {fragment_python-1.0.0/fragment/sdk → fragment_python-1.1.0/fragment/sync_sdk}/input_types.py +42 -16
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/sync_client.py +8 -5
- fragment_python-1.1.0/fragment/sync_sdk/typed_entries.py +95 -0
- fragment_python-1.1.0/pyproject.toml +85 -0
- fragment_python-1.0.0/fragment/codegen/main.py +0 -71
- fragment_python-1.0.0/pyproject.toml +0 -45
- {fragment_python-1.0.0 → fragment_python-1.1.0}/LICENSE +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/__init__.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/client/__init__.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/codegen/__init__.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/codegen/plugins/__init__.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/codegen/plugins/generate_client_method.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/codegen/plugins/get_file_comment.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/logger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/add_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/add_ledger_entry_runtime.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/base_model.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/create_custom_currency.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/create_custom_link.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/create_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/delete_custom_txs.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/delete_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/delete_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_account_data_migrations.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_entries_to_migrate_for_ledger_account_data_migration.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_entries_to_migrate_for_ledger_entry_data_migration.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_entry_data_migrations.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_ledger_account_balance.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_ledger_account_balance_with_child_rollup.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_ledger_account_lines.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/get_workspace.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/list_ledger_account_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/list_ledger_accounts.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/list_ledger_entries.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/list_ledger_entry_group_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/list_multi_currency_ledger_account_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/migrate_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/reconcile_tx.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/reconcile_tx_runtime.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/reverse_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/store_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/sync_custom_accounts.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/sync_custom_txs.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/update_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sdk/update_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/add_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/add_ledger_entry_runtime.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/base_model.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/create_custom_currency.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/create_custom_link.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/create_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/delete_custom_txs.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/delete_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/delete_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_account_data_migrations.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_entries_to_migrate_for_ledger_account_data_migration.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_entries_to_migrate_for_ledger_entry_data_migration.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_entry_data_migrations.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_ledger_account_balance.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_ledger_account_balance_with_child_rollup.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_ledger_account_lines.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/get_workspace.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/list_ledger_account_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/list_ledger_accounts.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/list_ledger_entries.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/list_ledger_entry_group_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/list_multi_currency_ledger_account_balances.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/migrate_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/reconcile_tx.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/reconcile_tx_runtime.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/reverse_ledger_entry.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/store_schema.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/sync_custom_accounts.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/sync_custom_txs.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/update_ledger.py +0 -0
- {fragment_python-1.0.0 → fragment_python-1.1.0}/fragment/sync_sdk/update_ledger_entry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fragment-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python SDK for https://fragment.dev/
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
16
|
Requires-Dist: ariadne-codegen (>=0.17.2,<0.18.0)
|
|
17
|
-
Requires-Dist: authlib (>=1.6.
|
|
17
|
+
Requires-Dist: authlib (>=1.6.12,<2.0.0)
|
|
18
18
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
19
19
|
Requires-Dist: graphql-core (>=3.2.3,<4.0.0)
|
|
20
20
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
@@ -92,7 +92,7 @@ print(get_schema_result.schema_.json())
|
|
|
92
92
|
|
|
93
93
|
### Post a Ledger Entry
|
|
94
94
|
|
|
95
|
-
To [post](https://fragment.dev/
|
|
95
|
+
To [post](https://fragment.dev/guides/post-ledger-entries#post-to-the-api) a Ledger Entry defined in your Schema:
|
|
96
96
|
|
|
97
97
|
```python
|
|
98
98
|
await graphql_client.add_ledger_entry(
|
|
@@ -107,9 +107,38 @@ await graphql_client.add_ledger_entry(
|
|
|
107
107
|
)
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Post a batch of Ledger Entries
|
|
111
|
+
|
|
112
|
+
To [post](https://fragment.dev/guides/post-ledger-entries#batch-ledger-entries) a batch of Ledger Entries atomically:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from fragment.sdk.typed_entries import UserFundsAccountV1
|
|
116
|
+
|
|
117
|
+
await graphql_client.add_ledger_entries(
|
|
118
|
+
entries=[
|
|
119
|
+
UserFundsAccountV1(
|
|
120
|
+
ik="some-ik-1",
|
|
121
|
+
ledger_ik="your-ledger-ik",
|
|
122
|
+
posted="1968-01-01T16:45:00Z",
|
|
123
|
+
user_id="user-1",
|
|
124
|
+
funding_amount="20000",
|
|
125
|
+
),
|
|
126
|
+
UserFundsAccountV1(
|
|
127
|
+
ik="some-ik-2",
|
|
128
|
+
ledger_ik="your-ledger-ik",
|
|
129
|
+
posted="1968-01-01T16:45:00Z",
|
|
130
|
+
user_id="user-2",
|
|
131
|
+
funding_amount="20000",
|
|
132
|
+
),
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Construct the entries in the batch using the strongly-typed models for your Schema in the `typed_entries` module of your generated client.
|
|
138
|
+
|
|
110
139
|
### Read a Ledger Account's Balance
|
|
111
140
|
|
|
112
|
-
To read a Ledger Account's [balance](https://fragment.dev/
|
|
141
|
+
To read a Ledger Account's [balance](https://fragment.dev/guides/read-balances#latest):
|
|
113
142
|
|
|
114
143
|
```python
|
|
115
144
|
from fragment.sdk.enums import CurrencyCode
|
|
@@ -68,7 +68,7 @@ print(get_schema_result.schema_.json())
|
|
|
68
68
|
|
|
69
69
|
### Post a Ledger Entry
|
|
70
70
|
|
|
71
|
-
To [post](https://fragment.dev/
|
|
71
|
+
To [post](https://fragment.dev/guides/post-ledger-entries#post-to-the-api) a Ledger Entry defined in your Schema:
|
|
72
72
|
|
|
73
73
|
```python
|
|
74
74
|
await graphql_client.add_ledger_entry(
|
|
@@ -83,9 +83,38 @@ await graphql_client.add_ledger_entry(
|
|
|
83
83
|
)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
### Post a batch of Ledger Entries
|
|
87
|
+
|
|
88
|
+
To [post](https://fragment.dev/guides/post-ledger-entries#batch-ledger-entries) a batch of Ledger Entries atomically:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from fragment.sdk.typed_entries import UserFundsAccountV1
|
|
92
|
+
|
|
93
|
+
await graphql_client.add_ledger_entries(
|
|
94
|
+
entries=[
|
|
95
|
+
UserFundsAccountV1(
|
|
96
|
+
ik="some-ik-1",
|
|
97
|
+
ledger_ik="your-ledger-ik",
|
|
98
|
+
posted="1968-01-01T16:45:00Z",
|
|
99
|
+
user_id="user-1",
|
|
100
|
+
funding_amount="20000",
|
|
101
|
+
),
|
|
102
|
+
UserFundsAccountV1(
|
|
103
|
+
ik="some-ik-2",
|
|
104
|
+
ledger_ik="your-ledger-ik",
|
|
105
|
+
posted="1968-01-01T16:45:00Z",
|
|
106
|
+
user_id="user-2",
|
|
107
|
+
funding_amount="20000",
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Construct the entries in the batch using the strongly-typed models for your Schema in the `typed_entries` module of your generated client.
|
|
114
|
+
|
|
86
115
|
### Read a Ledger Account's Balance
|
|
87
116
|
|
|
88
|
-
To read a Ledger Account's [balance](https://fragment.dev/
|
|
117
|
+
To read a Ledger Account's [balance](https://fragment.dev/guides/read-balances#latest):
|
|
89
118
|
|
|
90
119
|
```python
|
|
91
120
|
from fragment.sdk.enums import CurrencyCode
|
|
@@ -35,17 +35,20 @@ class AsyncFragmentClient(AsyncBaseClient):
|
|
|
35
35
|
super().__init__(url=api_url, http_client=http_client)
|
|
36
36
|
|
|
37
37
|
self.auth_url = auth_url
|
|
38
|
-
self.expiration_time = None
|
|
39
|
-
self.token = None
|
|
38
|
+
self.expiration_time: Optional[float] = None
|
|
39
|
+
self.token: Optional[Dict[str, Any]] = None
|
|
40
40
|
self.oauth2_client = AsyncOAuth2Client(
|
|
41
41
|
client_id, client_secret, scope=auth_scope
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
async def refresh_token(self):
|
|
44
|
+
async def refresh_token(self) -> None:
|
|
45
45
|
now = time.time()
|
|
46
46
|
if self.expiration_time is None or self.expiration_time <= now:
|
|
47
|
-
self.token
|
|
48
|
-
|
|
47
|
+
# Held in a local because `self.token` is declared `dict | None`,
|
|
48
|
+
# so reading the attribute back is not narrowed by the assignment.
|
|
49
|
+
token = await self.oauth2_client.fetch_token(self.auth_url)
|
|
50
|
+
self.token = token
|
|
51
|
+
self.expiration_time = now + token["expires_in"]
|
|
49
52
|
|
|
50
53
|
async def execute(
|
|
51
54
|
self,
|
|
@@ -33,15 +33,18 @@ class SyncFragmentClient(BaseClient):
|
|
|
33
33
|
super().__init__(url=api_url, http_client=http_client)
|
|
34
34
|
|
|
35
35
|
self.auth_url = auth_url
|
|
36
|
-
self.expiration_time = None
|
|
37
|
-
self.token = None
|
|
36
|
+
self.expiration_time: Optional[float] = None
|
|
37
|
+
self.token: Optional[Dict[str, Any]] = None
|
|
38
38
|
self.oauth2_client = OAuth2Client(client_id, client_secret, scope=auth_scope)
|
|
39
39
|
|
|
40
|
-
def refresh_token(self):
|
|
40
|
+
def refresh_token(self) -> None:
|
|
41
41
|
now = time.time()
|
|
42
42
|
if self.expiration_time is None or self.expiration_time <= now:
|
|
43
|
-
self.token
|
|
44
|
-
|
|
43
|
+
# Held in a local because `self.token` is declared `dict | None`,
|
|
44
|
+
# so reading the attribute back is not narrowed by the assignment.
|
|
45
|
+
token = self.oauth2_client.fetch_token(self.auth_url)
|
|
46
|
+
self.token = token
|
|
47
|
+
self.expiration_time = now + token["expires_in"]
|
|
45
48
|
|
|
46
49
|
def execute(
|
|
47
50
|
self,
|
|
@@ -39,9 +39,16 @@ def get_codegen_config(
|
|
|
39
39
|
base_client_name=client_name,
|
|
40
40
|
base_client_file_path=get_project_path_relative_to_file(file_path),
|
|
41
41
|
async_client=False if use_sync_client else True,
|
|
42
|
+
# Order matters. GenerateTypedLedgerEntries copies annotations
|
|
43
|
+
# off the generated client methods, so it has to run after
|
|
44
|
+
# RewriteUnsetTypeMethodArguments has turned
|
|
45
|
+
# `Union[Optional[X], UnsetType]` into `Optional[X]`. Listed
|
|
46
|
+
# earlier, it emits `UnsetType` into a module that never imports
|
|
47
|
+
# it. collect_annotations raises if that ever happens.
|
|
42
48
|
plugins=[
|
|
43
49
|
"fragment.codegen.plugins.get_file_comment.GenerateFileComment",
|
|
44
50
|
"fragment.codegen.plugins.generate_client_method.RewriteUnsetTypeMethodArguments",
|
|
51
|
+
"fragment.codegen.plugins.generate_typed_entries.GenerateTypedLedgerEntries",
|
|
45
52
|
],
|
|
46
53
|
),
|
|
47
54
|
},
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import httpx
|
|
9
|
+
from ariadne_codegen.main import client as generate_graphql_client
|
|
10
|
+
|
|
11
|
+
from fragment.codegen.helpers import get_codegen_config, get_standard_queries
|
|
12
|
+
from fragment.logger import console_log
|
|
13
|
+
|
|
14
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
GRAPHQL_SCHEMA_API_URL = "https://api.us-west-2.fragment.dev/schema.graphql"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextlib.contextmanager
|
|
21
|
+
def resolved_schema_path(schema_path: str | None) -> Iterator[str]:
|
|
22
|
+
"""Yield a path to the schema to generate against.
|
|
23
|
+
|
|
24
|
+
A local path is used as-is, which keeps generation reproducible and offline.
|
|
25
|
+
Without one the current schema is downloaded to a temporary file, so output
|
|
26
|
+
depends on whatever the API looks like at that moment.
|
|
27
|
+
"""
|
|
28
|
+
if schema_path is not None:
|
|
29
|
+
console_log.info(f"Using the GraphQL schema at {schema_path}")
|
|
30
|
+
yield schema_path
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
console_log.info(f"Downloading the GraphQL schema from {GRAPHQL_SCHEMA_API_URL}")
|
|
34
|
+
try:
|
|
35
|
+
response = httpx.get(GRAPHQL_SCHEMA_API_URL)
|
|
36
|
+
except httpx.RequestError as error:
|
|
37
|
+
console_log.error(f"An error occurred while downloading the schema: {error}")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".graphql") as schema_file:
|
|
40
|
+
schema_file.write(response.text)
|
|
41
|
+
schema_file.flush()
|
|
42
|
+
yield schema_file.name
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.command()
|
|
46
|
+
@click.option(
|
|
47
|
+
"-i",
|
|
48
|
+
"--input-dir",
|
|
49
|
+
default=None,
|
|
50
|
+
help="Path to your Schema queries",
|
|
51
|
+
required=True,
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"-n",
|
|
55
|
+
"--target-package-name",
|
|
56
|
+
default="fragment_graphql_client",
|
|
57
|
+
help="The package name for the generated SDK",
|
|
58
|
+
required=False,
|
|
59
|
+
)
|
|
60
|
+
@click.option(
|
|
61
|
+
"-o",
|
|
62
|
+
"--output-dir",
|
|
63
|
+
default=None,
|
|
64
|
+
help="The output directory for the generated SDK. Defaults to CWD.",
|
|
65
|
+
required=False,
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"-s",
|
|
69
|
+
"--schema-path",
|
|
70
|
+
default=None,
|
|
71
|
+
type=click.Path(exists=True, dir_okay=False, readable=True),
|
|
72
|
+
help=(
|
|
73
|
+
"Path to a local GraphQL schema. Defaults to downloading the current "
|
|
74
|
+
"schema. Pass a file to make generation reproducible and offline."
|
|
75
|
+
),
|
|
76
|
+
required=False,
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--sync",
|
|
80
|
+
help="Generate a synchronous client. Defaults to async.",
|
|
81
|
+
required=False,
|
|
82
|
+
is_flag=True,
|
|
83
|
+
)
|
|
84
|
+
def run(input_dir, target_package_name, sync, output_dir=None, schema_path=None):
|
|
85
|
+
with resolved_schema_path(schema_path) as resolved, tempfile.NamedTemporaryFile(
|
|
86
|
+
dir=input_dir, mode="w", suffix=".graphql"
|
|
87
|
+
) as standard_query_file:
|
|
88
|
+
# Write and flush the standard queries to the provided input
|
|
89
|
+
standard_query_file.write(get_standard_queries())
|
|
90
|
+
standard_query_file.flush()
|
|
91
|
+
config_dict = get_codegen_config(
|
|
92
|
+
use_sync_client=sync,
|
|
93
|
+
schema_path=resolved,
|
|
94
|
+
queries_path=input_dir,
|
|
95
|
+
target_package_name=target_package_name,
|
|
96
|
+
target_package_path=output_dir,
|
|
97
|
+
)
|
|
98
|
+
generate_graphql_client(config_dict)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ariadne_codegen.plugins.base import Plugin
|
|
5
|
+
from graphql import OperationDefinitionNode
|
|
6
|
+
|
|
7
|
+
from fragment.codegen.typed_entries import (
|
|
8
|
+
MODULE_NAME,
|
|
9
|
+
EntrySpec,
|
|
10
|
+
collect_annotations,
|
|
11
|
+
extract_entry_spec,
|
|
12
|
+
render_module,
|
|
13
|
+
resolve_class_names,
|
|
14
|
+
)
|
|
15
|
+
from fragment.logger import console_log
|
|
16
|
+
|
|
17
|
+
ADD_LEDGER_ENTRIES_OPERATION = "addLedgerEntries"
|
|
18
|
+
ENTRIES_ARGUMENT = "entries"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GenerateTypedLedgerEntries(Plugin):
|
|
22
|
+
"""Emit strongly-typed `addLedgerEntries` payload models.
|
|
23
|
+
|
|
24
|
+
`addLedgerEntries` accepts a list of a single input type whose `parameters`
|
|
25
|
+
field is an opaque `JSON` scalar, so GraphQL alone cannot type an individual
|
|
26
|
+
entry in a batch. The per-entry-type `addLedgerEntry` operations already in
|
|
27
|
+
the input queries do carry that information, so this plugin recovers it and
|
|
28
|
+
renders one pydantic model per entry type into a `typed_entries` module.
|
|
29
|
+
|
|
30
|
+
Specs are collected in `generate_client_method`, which ariadne calls for
|
|
31
|
+
every operation. The module is written in `generate_init_code`, the last
|
|
32
|
+
hook to run, by which point every operation has been seen.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, schema, config_dict: dict) -> None:
|
|
36
|
+
super().__init__(schema, config_dict)
|
|
37
|
+
settings = config_dict.get("tool", {}).get("ariadne-codegen", {})
|
|
38
|
+
self.package_path = Path(
|
|
39
|
+
settings.get("target_package_path", Path.cwd())
|
|
40
|
+
) / settings.get("target_package_name", "graphql_client")
|
|
41
|
+
self.specs: list[EntrySpec] = []
|
|
42
|
+
# Both names below are owned upstream in fragment-dev/graphql-queries,
|
|
43
|
+
# so track whether each was actually seen rather than assuming.
|
|
44
|
+
self.saw_batch_operation = False
|
|
45
|
+
self.widened_entries_argument = False
|
|
46
|
+
|
|
47
|
+
def generate_client_method(
|
|
48
|
+
self,
|
|
49
|
+
method_def: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
50
|
+
operation_definition: OperationDefinitionNode,
|
|
51
|
+
) -> ast.FunctionDef | ast.AsyncFunctionDef:
|
|
52
|
+
annotations: dict[str, str] = collect_annotations(
|
|
53
|
+
method_def, operation_definition
|
|
54
|
+
)
|
|
55
|
+
spec = extract_entry_spec(operation_definition, annotations)
|
|
56
|
+
if spec is not None:
|
|
57
|
+
self.specs.append(spec)
|
|
58
|
+
if (
|
|
59
|
+
operation_definition.name
|
|
60
|
+
and operation_definition.name.value == ADD_LEDGER_ENTRIES_OPERATION
|
|
61
|
+
):
|
|
62
|
+
self.saw_batch_operation = True
|
|
63
|
+
self._widen_entries_argument(method_def)
|
|
64
|
+
return method_def
|
|
65
|
+
|
|
66
|
+
def _widen_entries_argument(
|
|
67
|
+
self, method_def: ast.FunctionDef | ast.AsyncFunctionDef
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Let `add_ledger_entries` take typed entries as well as raw inputs.
|
|
70
|
+
|
|
71
|
+
ariadne annotates the argument `list[AddLedgerEntryInput]`, which typed
|
|
72
|
+
entries satisfy at runtime but not under a type checker. Widening to a
|
|
73
|
+
`Sequence` of either keeps raw inputs working while accepting typed
|
|
74
|
+
models directly -- `Sequence` because `list` is invariant, so
|
|
75
|
+
`list[AuthCapture]` would otherwise be rejected.
|
|
76
|
+
|
|
77
|
+
Widening the annotation alone would be a runtime trap. The base client
|
|
78
|
+
recurses into variables with `isinstance(value, list)`, so a tuple would
|
|
79
|
+
satisfy the annotation, skip conversion, and reach `json.dumps` as model
|
|
80
|
+
objects. `_coerce_entries_to_list` closes that.
|
|
81
|
+
"""
|
|
82
|
+
for arg in method_def.args.args:
|
|
83
|
+
if arg.arg != ENTRIES_ARGUMENT:
|
|
84
|
+
continue
|
|
85
|
+
# Parsed rather than hand-built: an ast.Name whose id is an entire
|
|
86
|
+
# expression unparses fine but is not a valid tree, so anything that
|
|
87
|
+
# visits or compiles it breaks.
|
|
88
|
+
arg.annotation = ast.parse(
|
|
89
|
+
"Sequence[Union[AddLedgerEntryInput, TypedLedgerEntry]]",
|
|
90
|
+
mode="eval",
|
|
91
|
+
).body
|
|
92
|
+
self._coerce_entries_to_list(method_def)
|
|
93
|
+
self.widened_entries_argument = True
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
console_log.warning(
|
|
97
|
+
"Could not find an %r argument on the generated add_ledger_entries "
|
|
98
|
+
"method, so its signature was left as-is. Typed entry payloads will "
|
|
99
|
+
"still serialise correctly but will not typecheck when passed to it.",
|
|
100
|
+
ENTRIES_ARGUMENT,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _coerce_entries_to_list(
|
|
104
|
+
self, method_def: ast.FunctionDef | ast.AsyncFunctionDef
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Rewrite `{"entries": entries}` to `{"entries": list(entries)}`.
|
|
107
|
+
|
|
108
|
+
The base client only recurses into `list`, so any other sequence reaches
|
|
109
|
+
the JSON encoder holding model objects. Widening the annotation is what
|
|
110
|
+
makes that reachable, so the coercion belongs with it.
|
|
111
|
+
"""
|
|
112
|
+
for node in ast.walk(method_def):
|
|
113
|
+
if not isinstance(node, ast.Dict):
|
|
114
|
+
continue
|
|
115
|
+
for index, key in enumerate(node.keys):
|
|
116
|
+
if not (
|
|
117
|
+
isinstance(key, ast.Constant) and key.value == ENTRIES_ARGUMENT
|
|
118
|
+
):
|
|
119
|
+
continue
|
|
120
|
+
value = node.values[index]
|
|
121
|
+
if isinstance(value, ast.Name) and value.id == ENTRIES_ARGUMENT:
|
|
122
|
+
node.values[index] = ast.Call(
|
|
123
|
+
func=ast.Name(id="list", ctx=ast.Load()),
|
|
124
|
+
args=[value],
|
|
125
|
+
keywords=[],
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
console_log.warning(
|
|
130
|
+
"Could not find the %r variables assignment in add_ledger_entries, so "
|
|
131
|
+
"it was left as-is. Passing a non-list sequence of entries will fail "
|
|
132
|
+
"to serialise.",
|
|
133
|
+
ENTRIES_ARGUMENT,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def generate_client_code(self, generated_code: str) -> str:
|
|
137
|
+
if not self.widened_entries_argument:
|
|
138
|
+
return generated_code
|
|
139
|
+
return self._insert_imports(
|
|
140
|
+
generated_code,
|
|
141
|
+
[
|
|
142
|
+
"from typing import Sequence",
|
|
143
|
+
f"from .{MODULE_NAME} import TypedLedgerEntry",
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _insert_imports(code: str, imports: list[str]) -> str:
|
|
149
|
+
"""Insert imports after the module's existing top-level import block."""
|
|
150
|
+
lines = code.splitlines()
|
|
151
|
+
last_import_line = 0
|
|
152
|
+
for node in ast.parse(code).body:
|
|
153
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
154
|
+
last_import_line = max(last_import_line, node.end_lineno or 0)
|
|
155
|
+
lines[last_import_line:last_import_line] = imports
|
|
156
|
+
return "\n".join(lines) + "\n"
|
|
157
|
+
|
|
158
|
+
def generate_init_code(self, generated_code: str) -> str:
|
|
159
|
+
if self.specs and not self.saw_batch_operation:
|
|
160
|
+
# Typed payloads exist but no batch operation was generated to take
|
|
161
|
+
# them. Silence here would leave users with models nothing accepts.
|
|
162
|
+
console_log.warning(
|
|
163
|
+
"Generated %d typed entry payload(s) but found no %r operation, "
|
|
164
|
+
"so no batch method accepts them. Has the operation been renamed "
|
|
165
|
+
"upstream?",
|
|
166
|
+
len(self.specs),
|
|
167
|
+
ADD_LEDGER_ENTRIES_OPERATION,
|
|
168
|
+
)
|
|
169
|
+
module_path = self.package_path / f"{MODULE_NAME}.py"
|
|
170
|
+
module_path.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
module_path.write_text(
|
|
172
|
+
self._add_comment(render_module(self.specs)), encoding="utf-8"
|
|
173
|
+
)
|
|
174
|
+
return generated_code + self._init_additions()
|
|
175
|
+
|
|
176
|
+
def _add_comment(self, code: str) -> str:
|
|
177
|
+
# This module is written directly rather than through ariadne's module
|
|
178
|
+
# pipeline, so it does not pass through the GenerateFileComment hook.
|
|
179
|
+
queries_path = (
|
|
180
|
+
self.config_dict.get("tool", {})
|
|
181
|
+
.get("ariadne-codegen", {})
|
|
182
|
+
.get("queries_path", "")
|
|
183
|
+
)
|
|
184
|
+
comment = "# Generated by fragment (with the help of ariadne-codegen)"
|
|
185
|
+
if queries_path:
|
|
186
|
+
comment += f"\n# Source: {queries_path}"
|
|
187
|
+
return f"{comment}\n\n{code}"
|
|
188
|
+
|
|
189
|
+
def _init_additions(self) -> str:
|
|
190
|
+
"""Re-export the typed models and extend `__all__`.
|
|
191
|
+
|
|
192
|
+
Resolves names itself; `resolve_class_names` being pure is what makes this
|
|
193
|
+
agree with the renderer without depending on hook order.
|
|
194
|
+
"""
|
|
195
|
+
names = ["TypedLedgerEntry", "to_entry_inputs"] + [
|
|
196
|
+
class_name for class_name, _ in resolve_class_names(self.specs)
|
|
197
|
+
]
|
|
198
|
+
names.sort()
|
|
199
|
+
imported = ",\n ".join(names)
|
|
200
|
+
exported = "\n".join(f' "{name}",' for name in names)
|
|
201
|
+
return (
|
|
202
|
+
f"\nfrom .{MODULE_NAME} import (\n {imported},\n)\n"
|
|
203
|
+
f"\n__all__ += [\n{exported}\n]\n"
|
|
204
|
+
)
|