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.
Files changed (207) hide show
  1. architect_py/__init__.py +18 -2
  2. architect_py/async_client.py +1089 -658
  3. architect_py/client.py +36 -33
  4. architect_py/client_interface.py +63 -0
  5. architect_py/common_types/__init__.py +6 -0
  6. architect_py/common_types/order_dir.py +91 -0
  7. architect_py/common_types/scalars.py +25 -0
  8. architect_py/common_types/tradable_product.py +59 -0
  9. architect_py/graphql_client/__init__.py +2 -0
  10. architect_py/graphql_client/client.py +3 -6
  11. architect_py/graphql_client/enums.py +5 -0
  12. architect_py/graphql_client/fragments.py +3 -6
  13. architect_py/graphql_client/get_fills_query.py +2 -1
  14. architect_py/graphql_client/search_symbols_query.py +2 -1
  15. architect_py/graphql_client/subscribe_orderflow.py +2 -1
  16. architect_py/graphql_client/subscribe_trades.py +2 -1
  17. architect_py/grpc/__init__.py +145 -0
  18. architect_py/grpc/client.py +94 -0
  19. architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
  20. architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
  21. architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
  22. architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
  23. architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
  24. architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
  25. architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
  26. architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
  27. architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
  28. architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
  29. architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
  30. architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
  31. architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
  32. architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
  33. architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
  34. architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
  35. architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
  36. architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
  37. architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
  38. architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
  39. architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
  40. architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
  41. architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
  42. architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
  43. architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
  44. architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
  45. architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
  46. architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
  47. architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
  48. architect_py/grpc/models/Core/ConfigRequest.py +37 -0
  49. architect_py/grpc/models/Core/ConfigResponse.py +25 -0
  50. architect_py/grpc/models/Core/__init__.py +2 -0
  51. architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +5 -4
  52. architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
  53. architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
  54. architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
  55. architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
  56. architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
  57. architect_py/grpc/models/Cpty/__init__.py +2 -0
  58. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
  59. architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
  60. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
  61. architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
  62. architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
  63. architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
  64. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
  65. architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
  66. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
  67. architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
  68. architect_py/grpc/models/Folio/__init__.py +2 -0
  69. architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
  70. architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
  71. architect_py/grpc/models/Health/__init__.py +2 -0
  72. architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
  73. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
  74. architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
  75. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
  76. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
  77. architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
  78. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
  79. architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
  80. architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
  81. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
  82. architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
  83. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
  84. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
  85. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
  86. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
  87. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
  88. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
  89. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
  90. architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
  91. architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +14 -3
  92. architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
  93. architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
  94. architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
  95. architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
  96. architect_py/grpc/models/Marketdata/__init__.py +2 -0
  97. architect_py/grpc/models/Oms/Cancel.py +90 -0
  98. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
  99. architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
  100. architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
  101. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
  102. architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
  103. architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
  104. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
  105. architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
  106. architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
  107. architect_py/grpc/models/Oms/__init__.py +2 -0
  108. architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
  109. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
  110. architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
  111. architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
  112. architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
  113. architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
  114. architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
  115. architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
  116. architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +2 -1
  117. architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
  118. architect_py/grpc/models/Orderflow/__init__.py +2 -0
  119. architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
  120. architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
  121. architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
  122. architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
  123. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
  124. architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
  125. architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
  126. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
  127. architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
  128. architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
  129. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
  130. architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
  131. architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
  132. architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
  133. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
  134. architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
  135. architect_py/grpc/models/Symbology/__init__.py +2 -0
  136. architect_py/grpc/models/__init__.py +2 -0
  137. architect_py/{grpc_client → grpc/models}/definitions.py +671 -934
  138. architect_py/grpc/resolve_endpoint.py +70 -0
  139. architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
  140. architect_py/grpc/utils.py +32 -0
  141. architect_py/internal_utils/__init__.py +0 -0
  142. architect_py/internal_utils/no_pandas.py +3 -0
  143. architect_py/tests/conftest.py +91 -85
  144. architect_py/tests/test_book_building.py +49 -50
  145. architect_py/tests/test_marketdata.py +168 -0
  146. architect_py/tests/test_order_entry.py +37 -0
  147. architect_py/tests/test_orderflow.py +41 -0
  148. architect_py/tests/test_portfolio_management.py +23 -0
  149. architect_py/tests/test_rounding.py +28 -28
  150. architect_py/tests/test_symbology.py +37 -30
  151. architect_py/utils/nearest_tick.py +2 -5
  152. architect_py/utils/nearest_tick_2.py +1 -2
  153. architect_py/utils/orderbook.py +35 -0
  154. architect_py/utils/pandas.py +44 -0
  155. architect_py/utils/price_bands.py +0 -3
  156. architect_py/utils/symbol_parsing.py +29 -0
  157. architect_py-5.0.0.dist-info/METADATA +54 -0
  158. architect_py-5.0.0.dist-info/RECORD +214 -0
  159. {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
  160. architect_py-5.0.0.dist-info/top_level.txt +4 -0
  161. examples/__init__.py +0 -0
  162. examples/book_subscription.py +52 -0
  163. examples/candles.py +30 -0
  164. examples/common.py +116 -0
  165. examples/external_cpty.py +77 -0
  166. examples/funding_rate_mean_reversion_algo.py +186 -0
  167. examples/order_sending.py +91 -0
  168. examples/stream_l1_marketdata.py +25 -0
  169. examples/stream_l2_marketdata.py +38 -0
  170. examples/trades.py +21 -0
  171. examples/tutorial_async.py +86 -0
  172. examples/tutorial_sync.py +95 -0
  173. scripts/generate_functions_md.py +166 -0
  174. scripts/generate_sync_interface.py +226 -0
  175. scripts/postprocess_grpc.py +604 -0
  176. scripts/preprocess_grpc_schema.py +708 -0
  177. templates/exceptions.py +83 -0
  178. templates/juniper_base_client.py +371 -0
  179. architect_py/client_protocol.py +0 -52
  180. architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
  181. architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
  182. architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
  183. architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
  184. architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
  185. architect_py/grpc_client/Health/__init__.py +0 -2
  186. architect_py/grpc_client/Marketdata/__init__.py +0 -2
  187. architect_py/grpc_client/Oms/Cancel.py +0 -42
  188. architect_py/grpc_client/Oms/__init__.py +0 -2
  189. architect_py/grpc_client/Orderflow/__init__.py +0 -2
  190. architect_py/grpc_client/Symbology/__init__.py +0 -2
  191. architect_py/grpc_client/__init__.py +0 -2
  192. architect_py/grpc_client/grpc_client.py +0 -405
  193. architect_py/scalars.py +0 -172
  194. architect_py/tests/test_accounts.py +0 -31
  195. architect_py/tests/test_client.py +0 -29
  196. architect_py/tests/test_grpc_client.py +0 -30
  197. architect_py/tests/test_order_sending.py +0 -61
  198. architect_py/tests/test_snapshots.py +0 -52
  199. architect_py/tests/test_subscriptions.py +0 -129
  200. architect_py-3.2.1.dist-info/METADATA +0 -212
  201. architect_py-3.2.1.dist-info/RECORD +0 -146
  202. /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
  203. /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
  204. /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
  205. /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
  206. /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
  207. {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)