architect-py 3.2.1__py3-none-any.whl → 5.0.0__py3-none-any.whl
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.
- architect_py/__init__.py +18 -2
- architect_py/async_client.py +1089 -658
- architect_py/client.py +36 -33
- architect_py/client_interface.py +63 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +91 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/__init__.py +2 -0
- architect_py/graphql_client/client.py +3 -6
- architect_py/graphql_client/enums.py +5 -0
- architect_py/graphql_client/fragments.py +3 -6
- architect_py/graphql_client/get_fills_query.py +2 -1
- architect_py/graphql_client/search_symbols_query.py +2 -1
- architect_py/graphql_client/subscribe_orderflow.py +2 -1
- architect_py/graphql_client/subscribe_trades.py +2 -1
- architect_py/grpc/__init__.py +145 -0
- architect_py/grpc/client.py +94 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
- architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
- architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
- architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
- architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
- architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
- architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
- architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
- architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
- architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
- architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
- architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
- architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
- architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
- architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
- architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
- architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
- architect_py/grpc/models/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/grpc/models/Core/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +5 -4
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
- architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
- architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
- architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
- architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
- architect_py/grpc/models/Cpty/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
- architect_py/grpc/models/Folio/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
- architect_py/grpc/models/Health/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +14 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
- architect_py/grpc/models/Marketdata/__init__.py +2 -0
- architect_py/grpc/models/Oms/Cancel.py +90 -0
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
- architect_py/grpc/models/Oms/__init__.py +2 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
- architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +2 -1
- architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
- architect_py/grpc/models/Orderflow/__init__.py +2 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
- architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
- architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
- architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
- architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
- architect_py/grpc/models/Symbology/__init__.py +2 -0
- architect_py/grpc/models/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/definitions.py +671 -934
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
- architect_py/grpc/utils.py +32 -0
- architect_py/internal_utils/__init__.py +0 -0
- architect_py/internal_utils/no_pandas.py +3 -0
- architect_py/tests/conftest.py +91 -85
- architect_py/tests/test_book_building.py +49 -50
- architect_py/tests/test_marketdata.py +168 -0
- architect_py/tests/test_order_entry.py +37 -0
- architect_py/tests/test_orderflow.py +41 -0
- architect_py/tests/test_portfolio_management.py +23 -0
- architect_py/tests/test_rounding.py +28 -28
- architect_py/tests/test_symbology.py +37 -30
- architect_py/utils/nearest_tick.py +2 -5
- architect_py/utils/nearest_tick_2.py +1 -2
- architect_py/utils/orderbook.py +35 -0
- architect_py/utils/pandas.py +44 -0
- architect_py/utils/price_bands.py +0 -3
- architect_py/utils/symbol_parsing.py +29 -0
- architect_py-5.0.0.dist-info/METADATA +54 -0
- architect_py-5.0.0.dist-info/RECORD +214 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
- architect_py-5.0.0.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +52 -0
- examples/candles.py +30 -0
- examples/common.py +116 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +186 -0
- examples/order_sending.py +91 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +38 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +86 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +166 -0
- scripts/generate_sync_interface.py +226 -0
- scripts/postprocess_grpc.py +604 -0
- scripts/preprocess_grpc_schema.py +708 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -52
- architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
- architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
- architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
- architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
- architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- architect_py/grpc_client/Oms/Cancel.py +0 -42
- architect_py/grpc_client/Oms/__init__.py +0 -2
- architect_py/grpc_client/Orderflow/__init__.py +0 -2
- architect_py/grpc_client/Symbology/__init__.py +0 -2
- architect_py/grpc_client/__init__.py +0 -2
- architect_py/grpc_client/grpc_client.py +0 -405
- architect_py/scalars.py +0 -172
- architect_py/tests/test_accounts.py +0 -31
- architect_py/tests/test_client.py +0 -29
- architect_py/tests/test_grpc_client.py +0 -30
- architect_py/tests/test_order_sending.py +0 -61
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -129
- architect_py-3.2.1.dist-info/METADATA +0 -212
- architect_py-3.2.1.dist-info/RECORD +0 -146
- /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,604 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
This script processes Python gRPC types definition files along with corresponding JSON files.
|
4
|
+
It is run after auto-generation of the Python files from JSON schema files.
|
5
|
+
It further processes the Python files to add metadata to variant types, fix enum member names,
|
6
|
+
update variant type aliases, adjust imports, and generate stubs.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import argparse
|
10
|
+
import importlib
|
11
|
+
import json
|
12
|
+
import multiprocessing
|
13
|
+
import os
|
14
|
+
import re
|
15
|
+
from collections import defaultdict
|
16
|
+
from typing import Annotated, List, Tuple, Union, get_args, get_origin
|
17
|
+
|
18
|
+
# --------------------------------------------------------------------
|
19
|
+
# Regular Expressions (Constants)
|
20
|
+
# --------------------------------------------------------------------
|
21
|
+
|
22
|
+
REMOVE_ORDERDIR_RE = re.compile(
|
23
|
+
r"^class\s+OrderDir\b.*?(?=^class\s+\w|\Z)", re.DOTALL | re.MULTILINE
|
24
|
+
)
|
25
|
+
IMPORT_FIX_RE = re.compile(r"^from\s+(\.+)(\w*)\s+import\s+(.+)$")
|
26
|
+
CLASS_HEADER_RE = re.compile(r"^(\s*)class\s+(\w+)\([^)]*Enum[^)]*\)\s*:")
|
27
|
+
MEMBER_RE = re.compile(r"^(\s*)(\w+)\s*=\s*([0-9]+)(.*)")
|
28
|
+
|
29
|
+
ALIAS_PATTERN_UNION = re.compile(
|
30
|
+
r"^(?P<alias>\w+)\s*=\s*Annotated\[\s*(?P<union>Union\[(?P<union_content>.*?)\])\s*,\s*Meta\((?P<meta>.*?)\)\s*,?\s*\]",
|
31
|
+
re.DOTALL | re.MULTILINE,
|
32
|
+
)
|
33
|
+
ALIAS_PATTERN_SINGLE = re.compile(
|
34
|
+
r"^(?P<alias>\w+)\s*=\s*Annotated\[\s*(?P<single_type>\w+(?:\.\w+)*)\s*,\s*Meta\((?P<meta>.*?)\)\s*,?\s*\]",
|
35
|
+
re.DOTALL | re.MULTILINE,
|
36
|
+
)
|
37
|
+
|
38
|
+
# --------------------------------------------------------------------
|
39
|
+
# Utility Functions
|
40
|
+
# --------------------------------------------------------------------
|
41
|
+
|
42
|
+
|
43
|
+
def read_file(file_path: str) -> str:
|
44
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
45
|
+
return f.read()
|
46
|
+
|
47
|
+
|
48
|
+
def write_file(file_path: str, content: str) -> None:
|
49
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
50
|
+
f.write(content)
|
51
|
+
|
52
|
+
|
53
|
+
def get_py_file_json_pairs(py_folder: str, json_folder: str) -> List[Tuple[str, dict]]:
|
54
|
+
"""
|
55
|
+
Traverse json_folder recursively and yield pairs (py_file, json_data)
|
56
|
+
where py_file is the corresponding Python file in py_folder and json_data is
|
57
|
+
the JSON content loaded from the JSON file in json_folder.
|
58
|
+
"""
|
59
|
+
pairs = []
|
60
|
+
for root, dirs, files in os.walk(json_folder):
|
61
|
+
for file in files:
|
62
|
+
json_file_path = os.path.join(root, file)
|
63
|
+
rel_path = os.path.relpath(json_file_path, json_folder)
|
64
|
+
py_rel_path = f"{os.path.splitext(rel_path)[0]}.py"
|
65
|
+
py_file = os.path.join(py_folder, py_rel_path)
|
66
|
+
|
67
|
+
with open(json_file_path, "r", encoding="utf-8") as f:
|
68
|
+
json_data: dict = json.load(f)
|
69
|
+
|
70
|
+
# print(f"{json_file_path}: {py_file}")
|
71
|
+
pairs.append((py_file, json_data))
|
72
|
+
return pairs
|
73
|
+
|
74
|
+
|
75
|
+
def add_to_typing_import(file_str: str, type_to_add: str) -> str:
|
76
|
+
"""
|
77
|
+
Ensures that `type_to_add` is imported from typing.
|
78
|
+
If an import line exists, it adds the type if missing;
|
79
|
+
otherwise, it prepends a new import statement.
|
80
|
+
"""
|
81
|
+
typing_import_re = re.compile(
|
82
|
+
r"^(?P<indent>\s*)from\s+typing\s+import\s+(?P<imports>.+)$", re.MULTILINE
|
83
|
+
)
|
84
|
+
found = False
|
85
|
+
|
86
|
+
def replace_func(match):
|
87
|
+
nonlocal found
|
88
|
+
found = True
|
89
|
+
indent = match.group("indent")
|
90
|
+
imports = match.group("imports")
|
91
|
+
items = [item.strip() for item in imports.split(",")]
|
92
|
+
if type_to_add not in items:
|
93
|
+
items.append(type_to_add)
|
94
|
+
return f"{indent}from typing import {', '.join(items)}"
|
95
|
+
return match.group(0)
|
96
|
+
|
97
|
+
new_file_str = typing_import_re.sub(replace_func, file_str)
|
98
|
+
|
99
|
+
if not found:
|
100
|
+
new_file_str = f"from typing import {type_to_add}\n" + new_file_str
|
101
|
+
|
102
|
+
return new_file_str
|
103
|
+
|
104
|
+
|
105
|
+
def split_top_level(s: str, delimiter: str = ",") -> List[str]:
|
106
|
+
"""
|
107
|
+
Splits string `s` at top-level (non-nested) occurrences of `delimiter`.
|
108
|
+
Avoids splitting inside nested brackets.
|
109
|
+
"""
|
110
|
+
parts = []
|
111
|
+
current = []
|
112
|
+
level = 0
|
113
|
+
for char in s:
|
114
|
+
if char in "[(":
|
115
|
+
level += 1
|
116
|
+
elif char in "])":
|
117
|
+
level -= 1
|
118
|
+
if char == delimiter and level == 0:
|
119
|
+
parts.append("".join(current).strip())
|
120
|
+
current = []
|
121
|
+
else:
|
122
|
+
current.append(char)
|
123
|
+
if current:
|
124
|
+
parts.append("".join(current).strip())
|
125
|
+
return [part for part in parts if part]
|
126
|
+
|
127
|
+
|
128
|
+
def get_unannotated_type(t: type) -> type:
|
129
|
+
origin = get_origin(t)
|
130
|
+
if origin is Annotated:
|
131
|
+
t = get_args(t)[0]
|
132
|
+
|
133
|
+
origin = get_origin(t)
|
134
|
+
args = get_args(t)
|
135
|
+
if origin is not None and args:
|
136
|
+
# Rebuild the generic using the built-in generic type.
|
137
|
+
return origin[args]
|
138
|
+
|
139
|
+
return t
|
140
|
+
|
141
|
+
|
142
|
+
def get_type_str(t: type) -> str:
|
143
|
+
origin = get_origin(t)
|
144
|
+
if origin is Annotated:
|
145
|
+
t = get_unannotated_type(t)
|
146
|
+
origin = get_origin(t)
|
147
|
+
|
148
|
+
if origin is Union:
|
149
|
+
return " | ".join(get_type_str(arg) for arg in get_args(t))
|
150
|
+
|
151
|
+
if origin is not None:
|
152
|
+
args = get_args(t)
|
153
|
+
if args:
|
154
|
+
arg_str = ", ".join(get_type_str(arg) for arg in args)
|
155
|
+
return f"{origin.__name__}[{arg_str}]"
|
156
|
+
else:
|
157
|
+
# If no arguments are found, just return the name of the origin
|
158
|
+
return origin.__name__
|
159
|
+
|
160
|
+
return t.__name__
|
161
|
+
|
162
|
+
|
163
|
+
def built_in_type(t: type) -> bool:
|
164
|
+
return getattr(t, "__module__", None) == "builtins"
|
165
|
+
|
166
|
+
|
167
|
+
def get_import_str(t: type) -> str:
|
168
|
+
origin = get_origin(t)
|
169
|
+
if origin is Annotated:
|
170
|
+
t = get_unannotated_type(t)
|
171
|
+
origin = get_origin(t)
|
172
|
+
|
173
|
+
if origin is Union:
|
174
|
+
args = get_args(t)
|
175
|
+
args = [arg for arg in args if not built_in_type(arg)]
|
176
|
+
return ", ".join(get_import_str(arg) for arg in args)
|
177
|
+
|
178
|
+
if origin is not None:
|
179
|
+
args = get_args(t)
|
180
|
+
args = [arg for arg in args if not built_in_type(arg)]
|
181
|
+
if args:
|
182
|
+
return ", ".join(get_import_str(arg) for arg in args)
|
183
|
+
else:
|
184
|
+
# If no arguments are found, just return the name of the origin
|
185
|
+
return origin.__name__
|
186
|
+
|
187
|
+
name = t.__name__
|
188
|
+
if "." in name:
|
189
|
+
return name.split(".")[-1]
|
190
|
+
else:
|
191
|
+
return name
|
192
|
+
|
193
|
+
|
194
|
+
# --------------------------------------------------------------------
|
195
|
+
# Core Processing Functions (Refactored to work with content strings)
|
196
|
+
# --------------------------------------------------------------------
|
197
|
+
|
198
|
+
|
199
|
+
def create_tagged_subtypes_for_variant_types(content: str, json_data: dict) -> str:
|
200
|
+
"""
|
201
|
+
Processes the file content for variant types with a tag.
|
202
|
+
Creates or updates subtype classes with tag metadata and rebuilds
|
203
|
+
the type alias so that the Union is replaced by the variant classes.
|
204
|
+
"""
|
205
|
+
|
206
|
+
tag_field = json_data.get("tag_field")
|
207
|
+
if tag_field is None:
|
208
|
+
return content
|
209
|
+
|
210
|
+
if "oneOf" not in json_data:
|
211
|
+
return content
|
212
|
+
|
213
|
+
# Build a mapping from base type name to its variants.
|
214
|
+
tag_field_map: dict[str, List[Tuple[str, str]]] = defaultdict(list)
|
215
|
+
for p in json_data["oneOf"]:
|
216
|
+
tag_field_map[p["title"]].append((p["variant_name"], p["tag_value"]))
|
217
|
+
|
218
|
+
# Match the type alias (Union or single type)
|
219
|
+
alias_match = ALIAS_PATTERN_UNION.search(content)
|
220
|
+
if alias_match:
|
221
|
+
union_content = alias_match.group("union_content")
|
222
|
+
meta = alias_match.group("meta")
|
223
|
+
types = split_top_level(union_content)
|
224
|
+
else:
|
225
|
+
alias_match = ALIAS_PATTERN_SINGLE.search(content)
|
226
|
+
if not alias_match:
|
227
|
+
print(f"No type alias found in the file {json_data['title']}.")
|
228
|
+
return content
|
229
|
+
types = [alias_match.group("single_type")]
|
230
|
+
meta = alias_match.group("meta")
|
231
|
+
content = add_to_typing_import(content, "Union")
|
232
|
+
|
233
|
+
new_union_types = []
|
234
|
+
additional_class_defs = ""
|
235
|
+
|
236
|
+
for base_type in types:
|
237
|
+
# Leave dictionary types unchanged.
|
238
|
+
if base_type.startswith("dict") or base_type.startswith("Dict"):
|
239
|
+
new_union_types.append(base_type)
|
240
|
+
continue
|
241
|
+
|
242
|
+
simple_name = base_type.split(".")[-1]
|
243
|
+
if simple_name in tag_field_map:
|
244
|
+
for variant_name, tag_value in tag_field_map[simple_name]:
|
245
|
+
pattern = re.compile(
|
246
|
+
rf"^(class\s+{variant_name}\s*\()(?P<inheritance>[^)]*)(\))(\s*:)",
|
247
|
+
re.MULTILINE,
|
248
|
+
)
|
249
|
+
|
250
|
+
def repl(m):
|
251
|
+
if "tag=" in m.group("inheritance"):
|
252
|
+
return m.group(0)
|
253
|
+
return (
|
254
|
+
f"{m.group(1)}{m.group('inheritance')}, omit_defaults=True, "
|
255
|
+
f'tag_field="{tag_field}", tag="{tag_value}"{m.group(3)}{m.group(4)}'
|
256
|
+
)
|
257
|
+
|
258
|
+
content, count = pattern.subn(repl, content)
|
259
|
+
if count == 0:
|
260
|
+
new_class_def = (
|
261
|
+
f"\n\nclass {variant_name}({base_type}, omit_defaults=True, "
|
262
|
+
f'tag_field="{tag_field}", tag="{tag_value}"):\n'
|
263
|
+
" pass\n"
|
264
|
+
)
|
265
|
+
additional_class_defs += new_class_def
|
266
|
+
new_union_types.append(variant_name)
|
267
|
+
else:
|
268
|
+
new_union_types.append(base_type)
|
269
|
+
|
270
|
+
# Rebuild the union type alias.
|
271
|
+
if ALIAS_PATTERN_UNION.search(content):
|
272
|
+
final_union = [t for t in types if t.split(".")[-1] not in tag_field_map]
|
273
|
+
final_union += new_union_types
|
274
|
+
seen = set()
|
275
|
+
final_union_ordered = [t for t in final_union if not (t in seen or seen.add(t))]
|
276
|
+
new_union_str = "Union[" + ", ".join(final_union_ordered) + "]"
|
277
|
+
else:
|
278
|
+
new_union_str = "Union[" + ", ".join(new_union_types) + "]"
|
279
|
+
|
280
|
+
# Reassemble the alias definition.
|
281
|
+
alias_match = ALIAS_PATTERN_UNION.search(content) or ALIAS_PATTERN_SINGLE.search(
|
282
|
+
content
|
283
|
+
)
|
284
|
+
if not alias_match:
|
285
|
+
print(
|
286
|
+
f"No type alias found for {json_data['title']} when reinserting subclass definitions."
|
287
|
+
)
|
288
|
+
return content
|
289
|
+
|
290
|
+
alias_name = alias_match.group("alias")
|
291
|
+
new_alias_text = f"{alias_name} = Annotated[{new_union_str}, Meta({meta})]"
|
292
|
+
|
293
|
+
alias_start_index = alias_match.start()
|
294
|
+
content = (
|
295
|
+
content[:alias_start_index]
|
296
|
+
+ additional_class_defs
|
297
|
+
+ "\n\n"
|
298
|
+
+ new_alias_text
|
299
|
+
+ content[alias_match.end() :]
|
300
|
+
)
|
301
|
+
return content
|
302
|
+
|
303
|
+
|
304
|
+
def fix_lines(content: str) -> str:
|
305
|
+
"""
|
306
|
+
Fixes type annotations and adds necessary imports.
|
307
|
+
"""
|
308
|
+
lines = content.splitlines(keepends=True)
|
309
|
+
|
310
|
+
if any("def timestamp(self) -> int:" in line for line in lines):
|
311
|
+
lines.insert(4, "from datetime import datetime, timezone\n")
|
312
|
+
|
313
|
+
lines = [line.replace("Dir", "OrderDir") for line in lines]
|
314
|
+
lines = [line.replace("definitions.OrderDir", "OrderDir") for line in lines]
|
315
|
+
lines = [line.replace("(Struct)", "(Struct, omit_defaults=True)") for line in lines]
|
316
|
+
|
317
|
+
if any("OrderDir" in line for line in lines):
|
318
|
+
lines.insert(4, "from architect_py.common_types import OrderDir\n")
|
319
|
+
|
320
|
+
content = "".join(lines)
|
321
|
+
content = REMOVE_ORDERDIR_RE.sub("", content)
|
322
|
+
return content
|
323
|
+
|
324
|
+
|
325
|
+
def add_post_processing_to_unflattened_types(content: str, json_data: dict) -> str:
|
326
|
+
"""
|
327
|
+
Adds a __post_init__ method to the flattened types to enforce field requirements.
|
328
|
+
"""
|
329
|
+
enum_variant_to_other_required_keys: dict[str, List[str]] = json_data.get(
|
330
|
+
"enum_variant_to_other_required_keys", {}
|
331
|
+
)
|
332
|
+
if len(enum_variant_to_other_required_keys) == 0:
|
333
|
+
return content
|
334
|
+
|
335
|
+
enum_tag = json_data[
|
336
|
+
"tag_field"
|
337
|
+
] # should not be empty if enum_variant_to_other_required_keys is not empty
|
338
|
+
|
339
|
+
class_title = json_data["title"]
|
340
|
+
|
341
|
+
properties = json_data["properties"]
|
342
|
+
|
343
|
+
lines = content.splitlines(keepends=True)
|
344
|
+
# Append __post_init__ method at the end
|
345
|
+
lines.append("\n def __post_init__(self):\n")
|
346
|
+
|
347
|
+
common_keys = set.intersection(
|
348
|
+
*map(set, enum_variant_to_other_required_keys.values())
|
349
|
+
)
|
350
|
+
union_keys = set.union(*map(set, enum_variant_to_other_required_keys.values()))
|
351
|
+
|
352
|
+
for i, (enum_value, required_keys) in enumerate(
|
353
|
+
enum_variant_to_other_required_keys.items()
|
354
|
+
):
|
355
|
+
conditional = "if" if i == 0 else "elif"
|
356
|
+
title = properties[enum_tag]["title"]
|
357
|
+
req_keys_subset = [key for key in required_keys if key not in common_keys]
|
358
|
+
should_be_empty_keys = list(union_keys - set(required_keys))
|
359
|
+
lines.append(
|
360
|
+
f' {conditional} self.{enum_tag} == "{enum_value}":\n'
|
361
|
+
f" if not all(getattr(self, key) is not None for key in {req_keys_subset}):\n"
|
362
|
+
f' raise ValueError(f"When field {enum_tag} ({title}) is of value {enum_value}, '
|
363
|
+
f'class {class_title} requires fields {req_keys_subset}")\n'
|
364
|
+
f" elif any(getattr(self, key) is not None for key in {should_be_empty_keys}):\n"
|
365
|
+
f' raise ValueError(f"When field {enum_tag} ({title}) is of value {enum_value}, '
|
366
|
+
f'class {class_title} should not have fields {should_be_empty_keys}")\n'
|
367
|
+
)
|
368
|
+
|
369
|
+
return "".join(lines)
|
370
|
+
|
371
|
+
|
372
|
+
def generate_stub(content: str, json_data: dict) -> str:
|
373
|
+
"""
|
374
|
+
Generates stub code for Request files linking Request type to Response type, service, and route.
|
375
|
+
"""
|
376
|
+
request_type_name: str | None = json_data.get("title")
|
377
|
+
if request_type_name is None or "Request" not in request_type_name:
|
378
|
+
return content
|
379
|
+
|
380
|
+
service = json_data["service"]
|
381
|
+
rpc_method = json_data["rpc_method"]
|
382
|
+
response_type_name = json_data["response_type"]
|
383
|
+
route = json_data["route"]
|
384
|
+
|
385
|
+
try:
|
386
|
+
response_type_module = importlib.import_module(
|
387
|
+
f"architect_py.grpc.models.{service}.{response_type_name}"
|
388
|
+
)
|
389
|
+
except ModuleNotFoundError as e:
|
390
|
+
raise ModuleNotFoundError(
|
391
|
+
f"{e}\n"
|
392
|
+
f"Module {response_type_name} not found in architect_py.grpc.models.{service}. "
|
393
|
+
)
|
394
|
+
except ImportError as e:
|
395
|
+
raise ImportError(
|
396
|
+
f"{e}\n"
|
397
|
+
f"Module {response_type_name} could not be imported from architect_py.grpc.models.{service}. "
|
398
|
+
)
|
399
|
+
ResponseType = getattr(response_type_module, response_type_name)
|
400
|
+
UnAnnotatedResponseType = get_unannotated_type(ResponseType)
|
401
|
+
unannotated_response_type_str = get_type_str(UnAnnotatedResponseType)
|
402
|
+
union_type_import_str = get_import_str(UnAnnotatedResponseType)
|
403
|
+
|
404
|
+
request_type_module = importlib.import_module(
|
405
|
+
f"architect_py.grpc.models.{service}.{request_type_name}"
|
406
|
+
)
|
407
|
+
RequestType = getattr(request_type_module, request_type_name)
|
408
|
+
UnAnnotatedRequestType = get_unannotated_type(RequestType)
|
409
|
+
unannotated_request_type_str = get_type_str(UnAnnotatedRequestType)
|
410
|
+
|
411
|
+
if get_origin(UnAnnotatedResponseType) is Union:
|
412
|
+
response_import_str = f"from architect_py.grpc.models.{service}.{response_type_name} import {response_type_name}, {union_type_import_str}\n"
|
413
|
+
elif get_origin(UnAnnotatedResponseType) is not None:
|
414
|
+
response_import_str = f"from architect_py.grpc.models.{service}.{response_type_name} import {response_type_name}, {union_type_import_str}\n"
|
415
|
+
else:
|
416
|
+
response_import_str = f"from architect_py.grpc.models.{service}.{response_type_name} import {response_type_name}\n"
|
417
|
+
|
418
|
+
lines = content.splitlines(keepends=True)
|
419
|
+
for i, line in enumerate(lines):
|
420
|
+
if line.startswith(' "SENTINAL_VALUE"'):
|
421
|
+
lines[i] = f"""
|
422
|
+
@staticmethod
|
423
|
+
def get_response_type():
|
424
|
+
return {response_type_name}
|
425
|
+
|
426
|
+
@staticmethod
|
427
|
+
def get_unannotated_response_type():
|
428
|
+
return {unannotated_response_type_str}
|
429
|
+
|
430
|
+
@staticmethod
|
431
|
+
def get_route() -> str:
|
432
|
+
return "{route}"
|
433
|
+
|
434
|
+
@staticmethod
|
435
|
+
def get_rpc_method():
|
436
|
+
return "{rpc_method}"
|
437
|
+
"""
|
438
|
+
|
439
|
+
# If this is a Request file, append additional gRPC info.
|
440
|
+
if (json_data.get("tag_field") is not None) and (
|
441
|
+
json_data.get("enum_variant_to_other_required_keys") is None
|
442
|
+
):
|
443
|
+
service = json_data["service"]
|
444
|
+
rpc_method = json_data["rpc_method"]
|
445
|
+
response_type = json_data["response_type"]
|
446
|
+
route = json_data["route"]
|
447
|
+
|
448
|
+
lines.append(f'\n\n{request_type_name}_rpc_method = "{rpc_method}"\n')
|
449
|
+
lines.append(
|
450
|
+
f"Unannotated{request_type_name} = {unannotated_request_type_str}\n"
|
451
|
+
)
|
452
|
+
lines.append(f"{request_type_name}ResponseType = {response_type}\n")
|
453
|
+
lines.append(
|
454
|
+
f"{request_type_name}UnannotatedResponseType = {unannotated_response_type_str}\n"
|
455
|
+
)
|
456
|
+
lines.append(f'{request_type_name}_route = "{route}"\n')
|
457
|
+
|
458
|
+
lines.insert(4, response_import_str)
|
459
|
+
return "".join(lines)
|
460
|
+
|
461
|
+
|
462
|
+
def fix_enum_member_names(content: str, json_data: dict) -> str:
|
463
|
+
"""
|
464
|
+
Fixes enum member names based on JSON definitions.
|
465
|
+
For each enum class (classes that inherit from Enum),
|
466
|
+
it replaces the member names with the names defined in the JSON file under "x-enumNames".
|
467
|
+
"""
|
468
|
+
|
469
|
+
lines = content.splitlines()
|
470
|
+
# Fix import statements from grpc classes.
|
471
|
+
for i, line in enumerate(lines):
|
472
|
+
if line.strip() != "from .. import definitions":
|
473
|
+
m = IMPORT_FIX_RE.match(line)
|
474
|
+
if m:
|
475
|
+
dots, module, imports = m.groups()
|
476
|
+
names = [name.strip() for name in imports.split(",")]
|
477
|
+
if module:
|
478
|
+
lines[i] = "\n".join(
|
479
|
+
f"from {dots}{module}.{name} import {name}" for name in names
|
480
|
+
)
|
481
|
+
else:
|
482
|
+
lines[i] = "\n".join(
|
483
|
+
f"from {dots}{name} import {name}" for name in names
|
484
|
+
)
|
485
|
+
|
486
|
+
new_lines = []
|
487
|
+
in_enum_class = False
|
488
|
+
enum_class_indent = ""
|
489
|
+
enum_values = []
|
490
|
+
enum_names = []
|
491
|
+
|
492
|
+
for line in lines:
|
493
|
+
stripped_line = line.rstrip("\n")
|
494
|
+
class_match = CLASS_HEADER_RE.match(stripped_line)
|
495
|
+
if class_match:
|
496
|
+
indent, class_name = class_match.groups()
|
497
|
+
if class_name in json_data:
|
498
|
+
in_enum_class = True
|
499
|
+
enum_class_indent = indent
|
500
|
+
enum_values = json_data[class_name].get("enum", [])
|
501
|
+
enum_names = json_data[class_name].get("x-enumNames", [])
|
502
|
+
else:
|
503
|
+
in_enum_class = False
|
504
|
+
new_lines.append(stripped_line)
|
505
|
+
continue
|
506
|
+
|
507
|
+
if in_enum_class:
|
508
|
+
member_match = MEMBER_RE.match(stripped_line)
|
509
|
+
if member_match:
|
510
|
+
member_indent, _, member_value, rest = member_match.groups()
|
511
|
+
if len(member_indent) <= len(enum_class_indent):
|
512
|
+
in_enum_class = False
|
513
|
+
new_lines.append(stripped_line)
|
514
|
+
continue
|
515
|
+
try:
|
516
|
+
value_int = int(member_value)
|
517
|
+
except ValueError:
|
518
|
+
new_lines.append(stripped_line)
|
519
|
+
continue
|
520
|
+
if value_int in enum_values:
|
521
|
+
index = enum_values.index(value_int)
|
522
|
+
new_line = (
|
523
|
+
f"{member_indent}{enum_names[index]} = {member_value}{rest}"
|
524
|
+
)
|
525
|
+
new_lines.append(new_line)
|
526
|
+
else:
|
527
|
+
new_lines.append(stripped_line)
|
528
|
+
else:
|
529
|
+
if stripped_line.strip() and not stripped_line.startswith(
|
530
|
+
" " * (len(enum_class_indent) + 1)
|
531
|
+
):
|
532
|
+
in_enum_class = False
|
533
|
+
new_lines.append(stripped_line)
|
534
|
+
else:
|
535
|
+
new_lines.append(stripped_line)
|
536
|
+
|
537
|
+
return "\n".join(new_lines)
|
538
|
+
|
539
|
+
|
540
|
+
# --------------------------------------------------------------------
|
541
|
+
# Main Entry Point (Processing Pipeline)
|
542
|
+
# --------------------------------------------------------------------
|
543
|
+
|
544
|
+
|
545
|
+
def main(py_file_path: str, json_folder: str) -> None:
|
546
|
+
num_cores = max(multiprocessing.cpu_count() - 1, 1)
|
547
|
+
|
548
|
+
py_fp_json_pairs: list[tuple[str, dict]] = get_py_file_json_pairs(
|
549
|
+
py_file_path, json_folder
|
550
|
+
)
|
551
|
+
|
552
|
+
print("[1/2] Post-processing...")
|
553
|
+
with multiprocessing.Pool(processes=num_cores) as pool:
|
554
|
+
_ = pool.starmap(part_1, py_fp_json_pairs)
|
555
|
+
|
556
|
+
print("[2/2] Post-processing...")
|
557
|
+
for py_fp, json_data in py_fp_json_pairs:
|
558
|
+
part_2(py_fp, json_data)
|
559
|
+
|
560
|
+
print("Post-processing complete.")
|
561
|
+
|
562
|
+
|
563
|
+
def part_1(py_file_path: str, json_data: dict) -> None:
|
564
|
+
content = read_file(py_file_path)
|
565
|
+
|
566
|
+
content = create_tagged_subtypes_for_variant_types(content, json_data)
|
567
|
+
content = fix_lines(content)
|
568
|
+
if not py_file_path.endswith("definitions.py"):
|
569
|
+
content = add_post_processing_to_unflattened_types(content, json_data)
|
570
|
+
|
571
|
+
content = fix_enum_member_names(content, json_data)
|
572
|
+
|
573
|
+
write_file(py_file_path, content)
|
574
|
+
|
575
|
+
|
576
|
+
def part_2(py_file_path: str, json_data: dict) -> None:
|
577
|
+
content = read_file(py_file_path)
|
578
|
+
if not py_file_path.endswith("definitions.py"):
|
579
|
+
"""
|
580
|
+
we write and then read the same file because we need to update the content for when we do the import for the variant types
|
581
|
+
"""
|
582
|
+
content = generate_stub(content, json_data)
|
583
|
+
else:
|
584
|
+
content = content.replace(' "SENTINAL_VALUE"', "")
|
585
|
+
|
586
|
+
write_file(py_file_path, content)
|
587
|
+
|
588
|
+
|
589
|
+
if __name__ == "__main__":
|
590
|
+
parser = argparse.ArgumentParser(description="Process gRPC service definitions")
|
591
|
+
parser.add_argument(
|
592
|
+
"--file_path",
|
593
|
+
type=str,
|
594
|
+
default="architect_py/grpc_client",
|
595
|
+
help="Path to the Python folder with the gRPC service definitions",
|
596
|
+
)
|
597
|
+
parser.add_argument(
|
598
|
+
"--json_folder",
|
599
|
+
type=str,
|
600
|
+
required=True,
|
601
|
+
help="Path to the processed_schema folder output by preprocess_grpc_types.py",
|
602
|
+
)
|
603
|
+
args = parser.parse_args()
|
604
|
+
main(args.file_path, args.json_folder)
|