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