architect-py 3.2.1__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- architect_py/__init__.py +18 -2
- architect_py/async_client.py +1089 -658
- architect_py/client.py +36 -33
- architect_py/client_interface.py +63 -0
- architect_py/common_types/__init__.py +6 -0
- architect_py/common_types/order_dir.py +91 -0
- architect_py/common_types/scalars.py +25 -0
- architect_py/common_types/tradable_product.py +59 -0
- architect_py/graphql_client/__init__.py +2 -0
- architect_py/graphql_client/client.py +3 -6
- architect_py/graphql_client/enums.py +5 -0
- architect_py/graphql_client/fragments.py +3 -6
- architect_py/graphql_client/get_fills_query.py +2 -1
- architect_py/graphql_client/search_symbols_query.py +2 -1
- architect_py/graphql_client/subscribe_orderflow.py +2 -1
- architect_py/graphql_client/subscribe_trades.py +2 -1
- architect_py/grpc/__init__.py +145 -0
- architect_py/grpc/client.py +94 -0
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Accounts/AccountsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Accounts/__init__.py +1 -1
- architect_py/grpc/models/Algo/AlgoOrder.py +114 -0
- architect_py/grpc/models/Algo/AlgoOrderRequest.py +46 -0
- architect_py/grpc/models/Algo/AlgoOrdersRequest.py +72 -0
- architect_py/grpc/models/Algo/AlgoOrdersResponse.py +27 -0
- architect_py/grpc/models/Algo/CreateAlgoOrderRequest.py +56 -0
- architect_py/grpc/models/Algo/PauseAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/PauseAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StartAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StartAlgoResponse.py +20 -0
- architect_py/grpc/models/Algo/StopAlgoRequest.py +42 -0
- architect_py/grpc/models/Algo/StopAlgoResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Algo/__init__.py +1 -1
- architect_py/grpc/models/Auth/CreateJwtRequest.py +47 -0
- architect_py/grpc/models/Auth/CreateJwtResponse.py +23 -0
- architect_py/{grpc_client/Cpty → grpc/models/Auth}/__init__.py +1 -1
- architect_py/grpc/models/Boss/DepositsRequest.py +40 -0
- architect_py/grpc/models/Boss/DepositsResponse.py +27 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsRequest.py +42 -0
- architect_py/grpc/models/Boss/RqdAccountStatisticsResponse.py +25 -0
- architect_py/grpc/models/Boss/StatementUrlRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementUrlResponse.py +23 -0
- architect_py/grpc/models/Boss/StatementsRequest.py +40 -0
- architect_py/grpc/models/Boss/StatementsResponse.py +27 -0
- architect_py/grpc/models/Boss/WithdrawalsRequest.py +40 -0
- architect_py/grpc/models/Boss/WithdrawalsResponse.py +27 -0
- architect_py/{grpc_client/Folio → grpc/models/Boss}/__init__.py +1 -1
- architect_py/grpc/models/Core/ConfigRequest.py +37 -0
- architect_py/grpc/models/Core/ConfigResponse.py +25 -0
- architect_py/grpc/models/Core/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Cpty/CptyRequest.py +5 -4
- architect_py/{grpc_client → grpc/models}/Cpty/CptyResponse.py +6 -6
- architect_py/grpc/models/Cpty/CptyStatus.py +48 -0
- architect_py/grpc/models/Cpty/CptyStatusRequest.py +45 -0
- architect_py/grpc/models/Cpty/CptysRequest.py +37 -0
- architect_py/grpc/models/Cpty/CptysResponse.py +27 -0
- architect_py/grpc/models/Cpty/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountHistoryResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummariesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummary.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/AccountSummaryRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsRequest.py +7 -4
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalFillsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersRequest.py +3 -3
- architect_py/{grpc_client → grpc/models}/Folio/HistoricalOrdersResponse.py +1 -1
- architect_py/grpc/models/Folio/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Health/HealthCheckResponse.py +1 -1
- architect_py/grpc/models/Health/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Marketdata/Candle.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesRequest.py +11 -8
- architect_py/{grpc_client → grpc/models}/Marketdata/HistoricalCandlesResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshot.py +52 -5
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotRequest.py +8 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshot.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/L2BookSnapshotRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Liquidation.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatus.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/MarketStatusRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeCurrentCandlesRequest.py +3 -4
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL1BookSnapshotsRequest.py +6 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeL2BookUpdatesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeLiquidationsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeManyCandlesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/SubscribeTradesRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/Ticker.py +14 -3
- architect_py/{grpc_client → grpc/models}/Marketdata/TickerRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Marketdata/TickersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Marketdata/Trade.py +2 -2
- architect_py/grpc/models/Marketdata/__init__.py +2 -0
- architect_py/grpc/models/Oms/Cancel.py +90 -0
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/CancelAllOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/CancelOrderRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/OpenOrdersResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/Order.py +6 -13
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Oms/PendingCancelsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Oms/PlaceOrderRequest.py +16 -23
- architect_py/grpc/models/Oms/__init__.py +2 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChain.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeks.py +30 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainGreeksRequest.py +47 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsChainRequest.py +45 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirations.py +29 -0
- architect_py/grpc/models/OptionsMarketdata/OptionsExpirationsRequest.py +42 -0
- architect_py/grpc/models/OptionsMarketdata/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/Orderflow/DropcopyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Orderflow/OrderflowRequest.py +2 -1
- architect_py/{grpc_client → grpc/models}/Orderflow/SubscribeOrderflowRequest.py +2 -2
- architect_py/grpc/models/Orderflow/__init__.py +2 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogRequest.py +42 -0
- architect_py/grpc/models/Symbology/DownloadProductCatalogResponse.py +27 -0
- architect_py/grpc/models/Symbology/ExecutionInfoRequest.py +47 -0
- architect_py/grpc/models/Symbology/ExecutionInfoResponse.py +27 -0
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/PruneExpiredSymbolsResponse.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SubscribeSymbology.py +1 -1
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologySnapshot.py +7 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbologyUpdate.py +9 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/SymbolsResponse.py +1 -1
- architect_py/grpc/models/Symbology/UploadProductCatalogRequest.py +49 -0
- architect_py/grpc/models/Symbology/UploadProductCatalogResponse.py +20 -0
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyRequest.py +2 -2
- architect_py/{grpc_client → grpc/models}/Symbology/UploadSymbologyResponse.py +1 -1
- architect_py/grpc/models/Symbology/__init__.py +2 -0
- architect_py/grpc/models/__init__.py +2 -0
- architect_py/{grpc_client → grpc/models}/definitions.py +671 -934
- architect_py/grpc/resolve_endpoint.py +70 -0
- architect_py/{grpc_client/grpc_server.py → grpc/server.py} +13 -9
- architect_py/grpc/utils.py +32 -0
- architect_py/internal_utils/__init__.py +0 -0
- architect_py/internal_utils/no_pandas.py +3 -0
- architect_py/tests/conftest.py +91 -85
- architect_py/tests/test_book_building.py +49 -50
- architect_py/tests/test_marketdata.py +168 -0
- architect_py/tests/test_order_entry.py +37 -0
- architect_py/tests/test_orderflow.py +41 -0
- architect_py/tests/test_portfolio_management.py +23 -0
- architect_py/tests/test_rounding.py +28 -28
- architect_py/tests/test_symbology.py +37 -30
- architect_py/utils/nearest_tick.py +2 -5
- architect_py/utils/nearest_tick_2.py +1 -2
- architect_py/utils/orderbook.py +35 -0
- architect_py/utils/pandas.py +44 -0
- architect_py/utils/price_bands.py +0 -3
- architect_py/utils/symbol_parsing.py +29 -0
- architect_py-5.0.0.dist-info/METADATA +54 -0
- architect_py-5.0.0.dist-info/RECORD +214 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info}/WHEEL +2 -1
- architect_py-5.0.0.dist-info/top_level.txt +4 -0
- examples/__init__.py +0 -0
- examples/book_subscription.py +52 -0
- examples/candles.py +30 -0
- examples/common.py +116 -0
- examples/external_cpty.py +77 -0
- examples/funding_rate_mean_reversion_algo.py +186 -0
- examples/order_sending.py +91 -0
- examples/stream_l1_marketdata.py +25 -0
- examples/stream_l2_marketdata.py +38 -0
- examples/trades.py +21 -0
- examples/tutorial_async.py +86 -0
- examples/tutorial_sync.py +95 -0
- scripts/generate_functions_md.py +166 -0
- scripts/generate_sync_interface.py +226 -0
- scripts/postprocess_grpc.py +604 -0
- scripts/preprocess_grpc_schema.py +708 -0
- templates/exceptions.py +83 -0
- templates/juniper_base_client.py +371 -0
- architect_py/client_protocol.py +0 -52
- architect_py/grpc_client/Algo/AlgoOrderForTwapAlgo.py +0 -61
- architect_py/grpc_client/Algo/CreateAlgoOrderRequestForTwapAlgo.py +0 -59
- architect_py/grpc_client/Algo/ModifyAlgoOrderRequestForTwapAlgo.py +0 -45
- architect_py/grpc_client/Folio/AggregatedAccountSummariesRequest.py +0 -59
- architect_py/grpc_client/Folio/AggregatedAccountSummariesResponse.py +0 -27
- architect_py/grpc_client/Health/__init__.py +0 -2
- architect_py/grpc_client/Marketdata/__init__.py +0 -2
- architect_py/grpc_client/Oms/Cancel.py +0 -42
- architect_py/grpc_client/Oms/__init__.py +0 -2
- architect_py/grpc_client/Orderflow/__init__.py +0 -2
- architect_py/grpc_client/Symbology/__init__.py +0 -2
- architect_py/grpc_client/__init__.py +0 -2
- architect_py/grpc_client/grpc_client.py +0 -405
- architect_py/scalars.py +0 -172
- architect_py/tests/test_accounts.py +0 -31
- architect_py/tests/test_client.py +0 -29
- architect_py/tests/test_grpc_client.py +0 -30
- architect_py/tests/test_order_sending.py +0 -61
- architect_py/tests/test_snapshots.py +0 -52
- architect_py/tests/test_subscriptions.py +0 -129
- architect_py-3.2.1.dist-info/METADATA +0 -212
- architect_py-3.2.1.dist-info/RECORD +0 -146
- /architect_py/{grpc_client → grpc/models}/Marketdata/ArrayOfL1BookSnapshot.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/L2BookUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Marketdata/TickerUpdate.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Dropcopy.py +0 -0
- /architect_py/{grpc_client → grpc/models}/Orderflow/Orderflow.py +0 -0
- {architect_py-3.2.1.dist-info → architect_py-5.0.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,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()
|