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,708 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import os
5
+ import re
6
+ from typing import Any, Dict, List
7
+
8
+ logging.basicConfig(level=logging.WARN)
9
+
10
+ # ---------------------------------------------------------------------
11
+ # Constants and Regular Expressions
12
+ # ---------------------------------------------------------------------
13
+
14
+ SNAKE_CASE_RE = re.compile(r"(?<!^)(?=[A-Z])")
15
+ EXTRACT_REF_RE = re.compile(r'("\$ref":\s*"#/definitions/)([^"]+)(")')
16
+
17
+ TAG_PATTERN = re.compile(r"<!--\s*py:\s*(.*?)\s*-->")
18
+
19
+
20
+ def replace_and_indent(match: re.Match) -> str:
21
+ indent = match.group(1)
22
+ return f'{indent}"type": "number",\n{indent}"format": "decimal"'
23
+
24
+
25
+ # ---------------------------------------------------------------------
26
+ # Type And Ref Fixing Functions
27
+ # ---------------------------------------------------------------------
28
+
29
+
30
+ def _apply_type_fixes_to_text(
31
+ text: str, is_definitions_file: bool, type_to_json_file: Dict[str, str]
32
+ ) -> str:
33
+ """
34
+ Perform string replacements and regex substitutions on the JSON text.
35
+ """
36
+
37
+ def replace_ref(match: re.Match) -> str:
38
+ prefix, class_title, suffix = match.groups()
39
+ if class_title in type_to_json_file:
40
+ ref = (
41
+ f"{type_to_json_file[class_title]}/#"
42
+ if is_definitions_file
43
+ else f"../{type_to_json_file[class_title]}/#"
44
+ )
45
+ else:
46
+ ref = (
47
+ f"#/{class_title}"
48
+ if is_definitions_file
49
+ else f"../definitions.json#/{class_title}"
50
+ )
51
+ return f'"$ref": "{ref}"'
52
+
53
+ return EXTRACT_REF_RE.sub(replace_ref, text)
54
+
55
+
56
+ def apply_type_fixes(
57
+ schema: Dict, is_definitions_file: bool, type_to_json_file: Dict[str, str]
58
+ ) -> Dict:
59
+ """
60
+ Convert the schema dictionary to text, apply type fixes, and convert it back.
61
+ """
62
+ json_text = json.dumps(schema, indent=2)
63
+ fixed_text = _apply_type_fixes_to_text(
64
+ json_text, is_definitions_file, type_to_json_file
65
+ )
66
+ return json.loads(fixed_text)
67
+
68
+
69
+ # ---------------------------------------------------------------------
70
+ # Schema Metadata and Enum Correction Functions
71
+ # ---------------------------------------------------------------------
72
+
73
+
74
+ def clean_metadata_from_description(schema: Dict):
75
+ text = schema.get("description", "")
76
+ new_description = TAG_PATTERN.sub("", text).strip()
77
+
78
+ if new_description:
79
+ schema["description"] = new_description
80
+ else:
81
+ schema.pop("description", None)
82
+
83
+ for definitions in schema.get("definitions", {}).values():
84
+ definitions["description"] = TAG_PATTERN.sub(
85
+ "", definitions.get("description", "")
86
+ ).strip()
87
+ if not definitions["description"]:
88
+ definitions.pop("description", None)
89
+
90
+
91
+ def parse_class_description(text: str) -> Dict[str, str]:
92
+ """
93
+ Parse metadata from a special comment in the description.
94
+ Expected format: <!-- py: key1=value1, key2=value2 -->
95
+ """
96
+ match = TAG_PATTERN.search(text)
97
+ if not match:
98
+ return {}
99
+
100
+ metadata_str = match.group(1)
101
+ metadata: Dict[str, str] = {}
102
+ for pair in metadata_str.split(","):
103
+ pair = pair.strip()
104
+ if not pair:
105
+ continue
106
+ if "=" not in pair:
107
+ raise ValueError(f"Malformed key-value pair: '{pair}'")
108
+ key, value = map(str.strip, pair.split("=", 1))
109
+ metadata[key] = value
110
+
111
+ return metadata
112
+
113
+
114
+ def correct_flattened_types(schema: Dict[str, Any]) -> None:
115
+ """
116
+ Processes any type that has
117
+ #[serde(flatten)]
118
+
119
+ because it would generate several types in the json
120
+ while we want 1 type.
121
+
122
+ Removes the oneOf list and merges common keys and additional properties.
123
+
124
+ if the flatten field is not the tag, enum_variant_to_other_required_keys
125
+ will be in the json but will be empty.
126
+ """
127
+ if "oneOf" not in schema or "required" not in schema:
128
+ return
129
+
130
+ one_of: List[Dict[str, Any]] = schema.pop("oneOf")
131
+ additional_properties: Dict[str, Any] = {}
132
+
133
+ enum_value_to_required: Dict[str, List[str]] = {}
134
+
135
+ description = schema.get("description", "")
136
+ metadata = parse_class_description(description)
137
+
138
+ try:
139
+ field_name, field_title, enum_type_name = metadata["unflatten"].split("/")
140
+ except KeyError:
141
+ raise KeyError(f"Missing 'flatten' metadata in {schema['title']}")
142
+
143
+ enum_tag_property: Dict[str, Any] = {
144
+ "type": "string",
145
+ "title": field_title,
146
+ "enum": [],
147
+ }
148
+
149
+ for item in one_of:
150
+ assert item.get("type") == "object", (
151
+ f"Expected object type in {schema['title']}"
152
+ )
153
+ properties = item.get("properties", {})
154
+ required = item.get("required", [])
155
+ for key, prop in properties.items():
156
+ if "enum" in prop:
157
+ [enum_value] = prop["enum"]
158
+ enum_tag_property["enum"].append(enum_value)
159
+ enum_value_to_required[enum_value] = required
160
+ else:
161
+ if key in additional_properties:
162
+ assert additional_properties[key] == prop, (
163
+ f"Conflicting properties for {key} in {schema['title']}"
164
+ )
165
+ else:
166
+ additional_properties[key] = prop
167
+
168
+ if not field_name:
169
+ raise ValueError(f"Enum value not found in {schema['title']}")
170
+
171
+ sets: list[set[str]] = [set(group["required"]) for group in one_of]
172
+ common_keys: list[str] = list(set.intersection(*sets)) if sets else []
173
+ common_keys.sort()
174
+ schema["required"].extend(common_keys)
175
+
176
+ if tag_field := metadata.get("tag"):
177
+ schema["tag_field"] = tag_field
178
+
179
+ schema["properties"].update(additional_properties)
180
+ schema["properties"][field_name] = {
181
+ "title": field_title,
182
+ "$ref": f"#/definitions/{enum_type_name}",
183
+ }
184
+
185
+ schema["definitions"][enum_type_name] = enum_tag_property
186
+ schema["enum_variant_to_other_required_keys"] = enum_value_to_required
187
+
188
+
189
+ def correct_variant_types(
190
+ schema: Dict[str, Any],
191
+ definitions: Dict[str, Any],
192
+ type_to_json_file: Dict[str, str],
193
+ ) -> None:
194
+ """
195
+ Process types that were Enums on the rust side.
196
+
197
+ This is because the Variants need both the Variant name and the actual type name.
198
+
199
+ pub enum Dropcopy {
200
+ #[serde(rename = "o")]
201
+ #[schemars(title = "Order|Order")]
202
+ Order(Order),
203
+ #[schemars(title = "Fill|Fill")]
204
+ #[serde(rename = "f")]
205
+ Fill(Fill),
206
+ #[serde(rename = "af")]
207
+ #[schemars(title = "AberrantFill|AberrantFill")]
208
+ AberrantFill(AberrantFill),
209
+ }
210
+ """
211
+
212
+ if "oneOf" not in schema or "required" in schema:
213
+ return
214
+
215
+ description = schema.get("description", "")
216
+ metadata = parse_class_description(description)
217
+
218
+ tag_field: str = metadata["tag"]
219
+ new_one_of: List[Dict[str, Any]] = []
220
+ for item in schema["oneOf"]:
221
+ item["required"].remove(tag_field)
222
+ [tag_value] = item["properties"].pop(tag_field)["enum"]
223
+ title = item.pop("title")
224
+
225
+ enum_ref = {
226
+ "tag_value": tag_value,
227
+ }
228
+ if "|" not in title:
229
+ type_name = title
230
+ if type_name in type_to_json_file:
231
+ enum_ref["variant_name"] = f"Tagged{title}"
232
+ enum_ref["$ref"] = f"../{type_to_json_file[type_name]}/#"
233
+ else:
234
+ enum_ref["variant_name"] = title
235
+ enum_ref.update(item)
236
+ enum_ref["title"] = title
237
+ else:
238
+ variant_name, type_name = title.split("|", 1)
239
+ enum_ref["title"] = type_name
240
+ if type_name in definitions:
241
+ if definitions[type_name] != item:
242
+ raise ValueError(f"Conflicting definitions for {type_name}.")
243
+ elif type_name in type_to_json_file:
244
+ pass
245
+ else:
246
+ definitions[type_name] = item
247
+
248
+ if type_name in type_to_json_file:
249
+ ref = f"../{type_to_json_file[type_name]}/#"
250
+ else:
251
+ ref = f"../definitions.json#/{type_name}"
252
+ enum_ref["$ref"] = ref
253
+ enum_ref["variant_name"] = (
254
+ f"Tagged{variant_name}" if variant_name == type_name else variant_name
255
+ )
256
+
257
+ new_one_of.append(enum_ref)
258
+
259
+ schema["oneOf"] = new_one_of
260
+ schema["tag_field"] = tag_field
261
+
262
+
263
+ def correct_enums_with_multiple_titles(schema: Dict[str, Any]) -> None:
264
+ """
265
+ "MinOrderQuantityUnit": {
266
+ "oneOf": [
267
+ {
268
+ "title": "Base",
269
+ "type": "object",
270
+ "required": [
271
+ "unit"
272
+ ],
273
+ "properties": {
274
+ "unit": {
275
+ "type": "string",
276
+ "enum": [
277
+ "base"
278
+ ]
279
+ }
280
+ }
281
+ },
282
+ {
283
+ "title": "Quote",
284
+ "type": "object",
285
+ "required": [
286
+ "unit"
287
+ ],
288
+ "properties": {
289
+ "unit": {
290
+ "type": "string",
291
+ "enum": [
292
+ "quote"
293
+ ]
294
+ }
295
+ }
296
+ }
297
+ ]
298
+ },
299
+ This output
300
+ class Base(Struct, omit_defaults=True):
301
+ unit: Literal["base"]
302
+ class Quote(Struct, omit_defaults=True):
303
+ unit: Literal["quote"]
304
+ which was redundant. This removes it to one class named after the ultimate type.
305
+ """
306
+ if "definitions" not in schema:
307
+ return
308
+
309
+ for type_name, definition in schema["definitions"].items():
310
+ one_of = definition.get("oneOf")
311
+ if not one_of or len(one_of) <= 1:
312
+ continue
313
+
314
+ first = one_of[0]
315
+ if not all(
316
+ item.get("type") == first["type"]
317
+ and item.get("required") == first.get("required")
318
+ and "properties" in item
319
+ and item["properties"].keys() == first["properties"].keys()
320
+ for item in one_of
321
+ ):
322
+ continue
323
+
324
+ prop_keys = first["properties"].keys()
325
+ if not all(
326
+ all(
327
+ item["properties"][key].get("type")
328
+ == first["properties"][key].get("type")
329
+ and "enum" in item["properties"][key]
330
+ for key in prop_keys
331
+ )
332
+ for item in one_of
333
+ ):
334
+ continue
335
+
336
+ # Consolidate the definition
337
+ merged_props = {
338
+ key: {
339
+ "type": first["properties"][key]["type"],
340
+ "enum": sorted(
341
+ {
342
+ enum_val
343
+ for item in one_of
344
+ for enum_val in item["properties"][key]["enum"]
345
+ }
346
+ ),
347
+ }
348
+ for key in prop_keys
349
+ }
350
+
351
+ schema["definitions"][type_name] = {
352
+ "title": type_name,
353
+ "type": first["type"],
354
+ "required": first["required"],
355
+ "properties": merged_props,
356
+ }
357
+
358
+
359
+ def correct_repr_enums_with_x_enumNames(schema: Dict[str, Any]) -> None:
360
+ """
361
+ This should currently not do anything
362
+ """
363
+ if "definitions" not in schema:
364
+ return
365
+
366
+ definitions: dict[str, Any] = schema["definitions"]
367
+ for t, definition in definitions.items():
368
+ if "x-enumNames" not in definition:
369
+ continue
370
+ description = definition.get("description", "")
371
+ metadata = parse_class_description(description)
372
+
373
+ if metadata.get("schema") == "repr":
374
+ assert definition["type"] == "integer"
375
+ enum_names: list[str] = definition["x-enumNames"]
376
+ enum_ints: list[int] = definition.pop("enum")
377
+ definition["old_enum"] = enum_ints
378
+ if len(enum_names) != len(enum_ints):
379
+ raise ValueError(
380
+ f"Enum names and values length mismatch in {t} in {schema['title']}"
381
+ )
382
+ definition["enum"] = enum_names
383
+ definition["type"] = "string"
384
+
385
+
386
+ def correct_enums_with_descriptions(schema: Dict[str, Any]) -> None:
387
+ """
388
+ Process enums that have descriptions in the schema.
389
+
390
+ If a enum value has a description, the json gets separated
391
+
392
+ See TimeInForce for an example
393
+ """
394
+ if "definitions" not in schema:
395
+ return
396
+
397
+ definitions: dict[str, Any] = schema["definitions"]
398
+ for t, definition in definitions.items():
399
+ if "oneOf" not in definition:
400
+ continue
401
+ one_of: list[dict[str, Any]] = definition["oneOf"]
402
+
403
+ new_enum = {
404
+ "enum": [],
405
+ "type": "string",
406
+ }
407
+ new_one_of = []
408
+ for item in one_of:
409
+ if "enum" not in item:
410
+ new_one_of.append(item)
411
+ continue
412
+
413
+ if item["type"] != "string":
414
+ raise ValueError(
415
+ f"Expected string type for enum in {t} in {schema['title']}"
416
+ )
417
+ new_enum["enum"].extend(item["enum"])
418
+
419
+ if len(new_enum["enum"]) > 0:
420
+ new_one_of.append(new_enum)
421
+
422
+ if len(new_one_of) == 1:
423
+ definition.pop("oneOf")
424
+ definition.update(new_one_of[0])
425
+ else:
426
+ definition["oneOf"] = new_one_of
427
+ new_enum["title"] = f"{t}Enum"
428
+
429
+
430
+ def correct_null_types_with_constraints(schema: Dict[str, Any]) -> None:
431
+ """
432
+ "title": "recv_time_ns",
433
+ "type": [
434
+ "integer",
435
+ "null"
436
+ ],
437
+ "format": "default",
438
+ "minimum": 0.0
439
+ in this case, there's an error when the type is potentially null and there's a constraint.
440
+ """
441
+ constraints = (
442
+ "exclusiveMinimum",
443
+ "minimum",
444
+ "exclusiveMaximum",
445
+ "maximum",
446
+ "multipleOf",
447
+ "minItems",
448
+ "maxItems",
449
+ "minLength",
450
+ "maxLength",
451
+ "pattern",
452
+ )
453
+
454
+ if "properties" in schema:
455
+ properties = schema["properties"]
456
+ for prop_def in properties.values():
457
+ if isinstance(prop_def, bool):
458
+ continue
459
+ if "type" in prop_def and "null" in prop_def["type"]:
460
+ for constraint in constraints:
461
+ if constraint in prop_def:
462
+ prop_def.pop(constraint)
463
+
464
+ if "definitions" in schema:
465
+ definitions: dict[str, Any] = schema["definitions"]
466
+ for definition in definitions.values():
467
+ properties = definition.get("properties", {})
468
+ for prop_def in properties.values():
469
+ if isinstance(prop_def, bool):
470
+ continue
471
+ if "type" in prop_def and "null" in prop_def["type"]:
472
+ for constraint in constraints:
473
+ if constraint in prop_def:
474
+ prop_def.pop(constraint)
475
+
476
+
477
+ def correct_type_from_description(schema: Dict[str, Any]) -> None:
478
+ """
479
+ on the rust side, due to
480
+ derive SerializeDisplay and DeserializeFromStr
481
+ classes can have different types than normal
482
+ in the case of OrderId, it is a string
483
+
484
+ "OrderId": {
485
+ "description": "System-unique, persistent order identifiers",
486
+ "type": "object",
487
+ "required": [
488
+ "seqid",
489
+ "seqno"
490
+ ],
491
+ "properties": {
492
+ "seqid": {
493
+ "type": "string",
494
+ "format": "uuid"
495
+ },
496
+ "seqno": {
497
+ "type": "integer",
498
+ "format": "default",
499
+ "minimum": 0.0
500
+ }
501
+ }
502
+ },
503
+ """
504
+ if "definitions" not in schema:
505
+ return
506
+
507
+ definitions: dict[str, Any] = schema["definitions"]
508
+ for t, definition in definitions.items():
509
+ description = definition.get("description", "")
510
+ metadata = parse_class_description(description)
511
+
512
+ new_type = metadata.get("type")
513
+ if new_type is not None:
514
+ definition["type"] = new_type
515
+ definition.pop("required")
516
+ definition.pop("properties")
517
+
518
+
519
+ def process_schema_definitions(
520
+ schema: Dict[str, Any],
521
+ definitions: Dict[str, Any],
522
+ type_to_json_file: Dict[str, str],
523
+ ) -> None:
524
+ """
525
+ Extract and process definitions from a schema.
526
+ """
527
+ correct_enums_with_multiple_titles(schema)
528
+ # correct_repr_enums_with_x_enumNames(schema)
529
+ correct_enums_with_descriptions(schema)
530
+ correct_variant_types(schema, definitions, type_to_json_file)
531
+ correct_flattened_types(schema)
532
+ correct_null_types_with_constraints(schema)
533
+ correct_type_from_description(schema)
534
+
535
+ clean_metadata_from_description(schema)
536
+
537
+ if "definitions" not in schema:
538
+ return
539
+
540
+ new_defs: dict[str, Any] = schema.pop("definitions")
541
+ for t, definition in new_defs.items():
542
+ if t in type_to_json_file:
543
+ continue
544
+ definitions[t] = definition
545
+
546
+
547
+ # ---------------------------------------------------------------------
548
+ # Utility Functions for Service and File Handling
549
+ # ---------------------------------------------------------------------
550
+
551
+
552
+ def capitalize_first_letter(word: str) -> str:
553
+ return word[0].upper() + word[1:] if word else word
554
+
555
+
556
+ def add_info_to_schema(services: List[Dict[str, Any]]) -> Dict[str, str]:
557
+ """
558
+ Enrich service definitions with metadata and build a mapping from type names to file paths.
559
+ """
560
+ type_to_json_file: Dict[str, str] = {}
561
+ for service in services:
562
+ service_name = service["name"]
563
+
564
+ for rpc in service["rpcs"]:
565
+ req_schema = rpc["request_type"]
566
+ resp_schema = rpc["response_type"]
567
+
568
+ # Add service-specific metadata.
569
+ req_schema["route"] = rpc["route"]
570
+ req_schema["rpc_method"] = rpc["type"]
571
+ req_schema["service"] = service_name
572
+
573
+ # Standardize titles.
574
+ response_type_name = "".join(
575
+ capitalize_first_letter(word)
576
+ for word in resp_schema["title"].split("_")
577
+ )
578
+ req_schema["response_type"] = response_type_name
579
+ resp_schema["title"] = response_type_name
580
+
581
+ req_title = "".join(
582
+ capitalize_first_letter(word) for word in req_schema["title"].split("_")
583
+ )
584
+ req_schema["title"] = req_title
585
+
586
+ type_to_json_file[req_title] = f"{service_name}/{req_title}.json"
587
+ type_to_json_file[response_type_name] = (
588
+ f"{service_name}/{response_type_name}.json"
589
+ )
590
+ return type_to_json_file
591
+
592
+
593
+ def write_json_file(data: Dict[Any, Any], path: str) -> None:
594
+ with open(path, "w") as out_file:
595
+ json.dump(data, out_file, indent=2)
596
+
597
+
598
+ def preprocess_rpc_schema(
599
+ schema: Dict[str, Any],
600
+ definitions: Dict[str, Any],
601
+ type_to_json_file: Dict[str, str],
602
+ is_definitions_file: bool = False,
603
+ ) -> Dict[str, Any]:
604
+ """
605
+ Process a schema by handling definitions and applying type fixes.
606
+ """
607
+ process_schema_definitions(schema, definitions, type_to_json_file)
608
+ return apply_type_fixes(schema, is_definitions_file, type_to_json_file)
609
+
610
+
611
+ def preprocess_service_schemas(
612
+ service: Dict[str, Any],
613
+ output_dir: str,
614
+ definitions: Dict[str, Any],
615
+ type_to_json_file: Dict[str, str],
616
+ ) -> None:
617
+ """
618
+ Pre-process each service schema.
619
+ Process each RPC within a service (both request and response) and write the resulting schema files.
620
+ """
621
+ service_name = service["name"]
622
+ service_dir = os.path.join(output_dir, service_name)
623
+ os.makedirs(service_dir, exist_ok=True)
624
+
625
+ for rpc in service["rpcs"]:
626
+ logging.debug(f"Processing {service_name} {rpc['type']} RPC: {rpc['route']}")
627
+ for key in ["request_type", "response_type"]:
628
+ schema = rpc[key]
629
+ logging.debug(f"Processing {schema['title']}")
630
+ processed_schema = preprocess_rpc_schema(
631
+ schema, definitions, type_to_json_file, False
632
+ )
633
+ schema_title = processed_schema["title"]
634
+ file_path = os.path.join(service_dir, f"{schema_title}.json")
635
+ write_json_file(processed_schema, file_path)
636
+
637
+
638
+ def preprocess_schemas(input_file: str, output_dir: str) -> None:
639
+ """
640
+ Rust build generates a JSON file containing RPC definitions and schemas
641
+ for each service. The top level object is an array of service definitions.
642
+
643
+ We split out each service and each schema within each service into separate
644
+ JSON schema files for post-processing. Simple corrections to the schema
645
+ are made here as well.
646
+ """
647
+ os.makedirs(output_dir, exist_ok=True)
648
+
649
+ with open(input_file, "r") as f:
650
+ input_s = f.read()
651
+
652
+ replacements = {
653
+ # Suppress UserWarning: format of 'uint32' not understood for 'integer' - using default
654
+ "uint32": "default",
655
+ "uint64": "default",
656
+ # Suppress UserWarning: format of 'int' not understood for 'integer' - using default
657
+ '"format": "int",': '"format": "default",',
658
+ '"format": "int"': '"format": "default"',
659
+ # Manually map partial-date-time format to time format (python str -> datetime.time)
660
+ '"format": "partial-date-time",': '"format": "time",',
661
+ '"format": "partial-date-time"': '"format": "time"',
662
+ # Add decimal format hint to all regex patterns that look like decimals;
663
+ # drop the original string pattern validation as redundant.
664
+ '"pattern": "^-?[0-9]+(\\\\.[0-9]+)?$",': '"format": "decimal",',
665
+ '"pattern": "^-?[0-9]+(\\\\.[0-9]+)?$"': '"format": "decimal"',
666
+ }
667
+
668
+ for old, new in replacements.items():
669
+ input_s = input_s.replace(old, new)
670
+
671
+ services = json.loads(input_s)
672
+
673
+ type_to_json_file = add_info_to_schema(services)
674
+ definitions: Dict[str, Any] = {}
675
+
676
+ for service in services:
677
+ logging.debug(f"Processing service {service['name']}")
678
+ preprocess_service_schemas(service, output_dir, definitions, type_to_json_file)
679
+
680
+ # we look at the line level to fix definitions
681
+ fixed_definitions = apply_type_fixes(definitions, True, type_to_json_file)
682
+ for t, definition in fixed_definitions.items():
683
+ if "enum_tag" in definition:
684
+ raise ValueError(
685
+ f"Enum tag found in definitions: {t}, please account for this in post-processing"
686
+ )
687
+ definitions_path = os.path.join(output_dir, "definitions.json")
688
+ write_json_file(fixed_definitions, definitions_path)
689
+
690
+
691
+ def main() -> None:
692
+ parser = argparse.ArgumentParser(description="Preprocess gRPC JSON schema file.")
693
+ parser.add_argument(
694
+ "--schema", type=str, required=True, help="JSON schema file to preprocess."
695
+ )
696
+ parser.add_argument(
697
+ "--output-dir",
698
+ type=str,
699
+ required=True,
700
+ help="Output path for preprocessed schema.",
701
+ )
702
+ args = parser.parse_args()
703
+ schema_file = os.path.expanduser(args.schema)
704
+ preprocess_schemas(schema_file, args.output_dir)
705
+
706
+
707
+ if __name__ == "__main__":
708
+ main()