This commit is contained in:
commit
664ee27835
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
eggs/
|
||||||
|
wheels/
|
||||||
|
*.whl
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage / pytest
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.tox/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# uv
|
||||||
|
.uv/
|
||||||
|
.uv_cache/
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Protobuf generated (if generated in-tree)
|
||||||
|
*_pb2.py
|
||||||
|
*_pb2_grpc.py
|
||||||
|
|
||||||
|
# IDE / editor
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project-specific
|
||||||
|
*.log
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.pytestEnabled": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Optivex
|
||||||
|
|
||||||
|
You are building **Optivex**, Optiver's own financial exchange. Your task is to design and implement an exchange supporting the features defined in the protobuf contracts in the `proto/` directory.
|
||||||
|
|
||||||
|
Clients of your exchange will connect via TCP to the public endpoints of your **components**. Each component can implement one or more protocols, which are defined in the `proto/` directory. Internally, you can organize your solution in whatever way you see fit, as long as you implement all the protocols defined in the `proto/` directory. The one exception is the **order book**: it must be a dedicated component that does not require authentication (`authRequired: false`), as it is an internal service that other components connect to.
|
||||||
|
|
||||||
|
A pre-populated [user data file](#user-data-file) will be provided, which is recreated automatically each day. Do not rely on any other form of persistent storage; all other data should be kept in memory.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
* A component is a TCP server listening to a single port. Each component may connect to other components via TCP.
|
||||||
|
* The original protocols may be extended with new messages and services, but they must remain compatible with the original version for the automated system tests to pass. The system tests will only exercise messages defined in the original `proto/` files — custom extensions will not be tested.
|
||||||
|
* The `request_id` field is used to match requests to responses. It must be set to the same value in the request and response messages.
|
||||||
|
* Every response message must contain an `error_message` field.
|
||||||
|
* Floating point numbers precision is 4 decimal places.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── proto/ # Protobuf contracts your components must implement
|
||||||
|
├── src/
|
||||||
|
│ ├── application/ # BaseApplication: config loading, logging, signal handling
|
||||||
|
│ │ ├── config_schema.json # JSON Schema for component runtime config
|
||||||
|
│ │ └── data_file_schema.json # JSON Schema for the user data file
|
||||||
|
│ ├── connection/ # TCP connectivity library
|
||||||
|
│ └── sample_app/ # Reference implementation — start here
|
||||||
|
├── deployment_config.json # Describes your components, protocols, and ports
|
||||||
|
├── deployment_config_schema.json # JSON Schema for the above
|
||||||
|
└── pyproject.toml # Project metadata, dependencies, and entry points
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Install dependencies and activate the virtual environment:
|
||||||
|
```
|
||||||
|
uv sync && source .venv/bin/activate
|
||||||
|
```
|
||||||
|
2. Run the sample app to see the framework in action:
|
||||||
|
```
|
||||||
|
sample-app -c src/sample_app/sample_app_config.json
|
||||||
|
```
|
||||||
|
See [`src/sample_app/README.md`](src/sample_app/README.md) for details.
|
||||||
|
|
||||||
|
3. Read [`proto/README.md`](proto/README.md) — it describes every message your components must implement, and is the best place to understand the full scope of the system.
|
||||||
|
|
||||||
|
## Key Concepts
|
||||||
|
|
||||||
|
### BaseApplication
|
||||||
|
|
||||||
|
`BaseApplication` (`src/application/application.py`) is the base class all your components should extend. It handles config loading and validation, logging setup, and graceful shutdown. Implement `_start()` in your subclass.
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
|
||||||
|
Each component must be registered as a **console-script** entry point in `pyproject.toml` under `[project.scripts]`. For example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.scripts]
|
||||||
|
sample-app = "sample_app.main:main"
|
||||||
|
my-order-book = "order_book.main:main"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `packageName` field in `deployment_config.json` must match the console-script name exactly (e.g. `"my-order-book"`). The test runner uses this name to locate and launch the component binary.
|
||||||
|
|
||||||
|
### Component Configuration
|
||||||
|
|
||||||
|
Each component's runtime config is validated against `src/application/config_schema.json`. The key fields are:
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `logLevel` | Yes | `DEBUG`, `INFO`, `WARN`, or `ERROR` |
|
||||||
|
| `logDirectory` | Yes | Directory where log files are written |
|
||||||
|
| `listenOn` | Yes | `{ host, port }` — address the component listens on |
|
||||||
|
| `connectTo` | No | Map of target component names to `{ host, port }` — keys must match the `name` field in [Deployment Config](#deployment-config) |
|
||||||
|
| `dataFilePath` | Yes | Path to the JSON user data file |
|
||||||
|
|
||||||
|
### User Data File
|
||||||
|
|
||||||
|
The user data file follows the schema in `src/application/data_file_schema.json`. It contains an array of users, each with `username`, `password`, and `full_name`. Components that require authentication should read this file to validate credentials.
|
||||||
|
|
||||||
|
### Deployment Config
|
||||||
|
|
||||||
|
`deployment_config.json` describes your full deployment. It has two top-level fields:
|
||||||
|
|
||||||
|
* **`components`** — an array of component configurations, each with:
|
||||||
|
* `name` — a unique identifier for the component.
|
||||||
|
* `packageName` — the console-script entry point name (see [Entry Points](#entry-points)).
|
||||||
|
* `protocols` — which protocols this component implements (`admin`, `order_book`, `info`, `execution`, `risk_limits`).
|
||||||
|
* `config` — the component's runtime config (see [Component Configuration](#component-configuration)).
|
||||||
|
* **`systemTests`** — which test suites to run (see [Incremental Testing](#incremental-testing)).
|
||||||
|
|
||||||
|
Full specification is available in `deployment_config_schema.json`.
|
||||||
|
|
||||||
|
## Deploying and Testing
|
||||||
|
|
||||||
|
Once ready to test, run `deploy.sh` from your project root to build and submit your implementation to the testing environment. This script builds the project using `uv build` and copies the results onto the UAT server, where automated system tests will be run against it. Test results will be copied back to your dev server under the `/tmp` directory.
|
||||||
|
|
||||||
|
### How Tests Run
|
||||||
|
|
||||||
|
The test runner reads your `deployment_config.json` to determine which components to start and which tests to run. For each test:
|
||||||
|
|
||||||
|
1. **Dependency resolution** — the required protocols are determined from the test's dependencies and the `connectTo` fields of your components.
|
||||||
|
2. **Start order** — components are started in dependency order (a component's `connectTo` targets are started first).
|
||||||
|
3. **Config overrides** — the test runner rewrites several fields in your component config at runtime: `listenOn` and `connectTo` ports are replaced with ephemeral ports, and `logDirectory` and `dataFilePath` are pointed to temporary paths. Do not hardcode any of these values in your component logic; always read them from the config.
|
||||||
|
4. **Startup** — each component must accept TCP connections on its assigned port within **2 seconds**, or the test will fail.
|
||||||
|
5. **Execution** — tests run with 4 parallel workers, so your components must handle concurrent connections. Each test simulates a single day of trading.
|
||||||
|
|
||||||
|
### Incremental Testing
|
||||||
|
|
||||||
|
The `systemTests` array controls which test suites are run. You can test incrementally by listing only the protocols you've implemented so far:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"systemTests": ["order_book"]
|
||||||
|
```
|
||||||
|
|
||||||
|
As you implement more protocols, add them to the array. The available test suites and their protocol dependencies are:
|
||||||
|
|
||||||
|
| Test Suite | Requires Protocols |
|
||||||
|
|---|---|
|
||||||
|
| `order_book` | `admin` |
|
||||||
|
| `info` | `admin`, `order_book` |
|
||||||
|
| `execution` | `admin`, `order_book` |
|
||||||
|
| `risk_limits` | `admin`, `execution` |
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generate Python files from proto files
|
||||||
|
mkdir -p src/proto
|
||||||
|
touch src/proto/.gitignore && echo "*" > src/proto/.gitignore
|
||||||
|
protoc --proto_path=proto --python_out=src/proto --mypy_out=src/proto proto/*.proto
|
||||||
|
|
||||||
|
# Adjust imports in generated Python files to use relative imports
|
||||||
|
find src/proto -name "*.py" -exec sed -i 's/^import \([^ ]*\)_pb2 as \([^ ]*\)$/from . import \1_pb2 as \2/' {} \;
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"packageName": "sample-app",
|
||||||
|
"protocols": ["admin"],
|
||||||
|
"authRequired": false,
|
||||||
|
"config": {
|
||||||
|
"logLevel": "DEBUG",
|
||||||
|
"logDirectory": "./logs",
|
||||||
|
"listenOn": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 9001
|
||||||
|
},
|
||||||
|
"connectTo": {
|
||||||
|
"core": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 9100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"packageName": "sample-app",
|
||||||
|
"protocols": [
|
||||||
|
"execution",
|
||||||
|
"info",
|
||||||
|
"order_book",
|
||||||
|
"risk_limits"
|
||||||
|
],
|
||||||
|
"authRequired": true,
|
||||||
|
"config": {
|
||||||
|
"logLevel": "DEBUG",
|
||||||
|
"logDirectory": "./logs",
|
||||||
|
"dataFilePath": "./data.json",
|
||||||
|
"listenOn": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 9100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"systemTests": [
|
||||||
|
"execution",
|
||||||
|
"info",
|
||||||
|
"order_book",
|
||||||
|
"risk_limits"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "/optiver/deployment_config.json",
|
||||||
|
"title": "DeploymentConfig",
|
||||||
|
"description": "Defines the set of components to deploy and, for each, which protocols it implements and its runtime configuration.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"components": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Ordered list of components to deploy. At least one component must be defined.",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ComponentConfig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systemTests": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "One or more system tests to run according to the protocols implemented by the components.",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/SystemTest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["components", "systemTests"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"Protocol": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A protocol defined in the proto/ directory that a component may implement.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"const": "admin",
|
||||||
|
"description": "Internal instrument admin protocol (optiver.exchange.internal.admin)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "execution",
|
||||||
|
"description": "Client-facing execution protocol (optiver.exchange.exec)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "info",
|
||||||
|
"description": "Market-data / information protocol (optiver.exchange.info)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "order_book",
|
||||||
|
"description": "Order-book protocol (optiver.exchange.orderbook)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "risk_limits",
|
||||||
|
"description": "Risk-limits protocol (optiver.exchange.risk)."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SystemTest": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A system test to run. The dependencies are the protocols that must be deployed for the test to run.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"const": "order_book",
|
||||||
|
"description": "Tests the order book protocol.",
|
||||||
|
"dependencies": ["admin"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "info",
|
||||||
|
"description": "Tests the info protocol.",
|
||||||
|
"dependencies": ["order_book", "admin"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "execution",
|
||||||
|
"description": "Tests the execution protocol.",
|
||||||
|
"dependencies": ["order_book", "admin"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "risk_limits",
|
||||||
|
"description": "Tests the risk limits protocol.",
|
||||||
|
"dependencies": ["execution", "admin"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ComponentConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configuration for a single deployable component.",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique human-readable identifier for this component (e.g. 'gateway', 'order_book_engine').",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"packageName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Console-script entry point installed by the wheel (e.g. 'my-exchange'). Used to locate the binary in the virtual environment.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"protocols": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "One or more protocols this component is responsible for implementing. Each value must correspond to a .proto file in the proto/ directory.",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/Protocol"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authRequired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Indicates whether authentication is required for this component.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"$ref": "src/application/config_schema.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "packageName", "protocols", "config"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Proto Definitions
|
||||||
|
|
||||||
|
This directory contains the Protobuf definitions that describe the entire messaging contract of the exchange system. Every message your components must accept, respond to, or emit is defined here.
|
||||||
|
|
||||||
|
These files are the **contract your implementation must satisfy** — system tests will connect to your components over TCP and exchange exactly these messages.
|
||||||
|
|
||||||
|
## Where to start
|
||||||
|
|
||||||
|
Begin with **`common.proto`**. It contains the `MessageType` enum, which is the single authoritative index of every message in the system. The enum is grouped by module (Auth, Admin, Info, Order Book, Execution, Risk), giving you a map of the whole system before you dive into any individual file. It also defines the shared types that appear everywhere else: `Instrument`, `Side`, and the auth `LoginRequest`/`LoginResponse` pair.
|
||||||
|
|
||||||
|
## File-by-file overview
|
||||||
|
|
||||||
|
### `common.proto`
|
||||||
|
The foundation. Defines:
|
||||||
|
- `MessageType` — a numbered enum of every request/response/notification in the system, grouped by subsystem.
|
||||||
|
- `Instrument` — the data model for a tradeable instrument (symbol, description, currency, multiplier).
|
||||||
|
- `Side` — the `BUY`/`SELL` enum used across order-related messages.
|
||||||
|
- `LoginRequest` / `LoginResponse` — the authentication handshake your components must perform with every connecting client before serving any other message.
|
||||||
|
|
||||||
|
Read this first to orient yourself. Every other file imports it.
|
||||||
|
|
||||||
|
### `admin.proto`
|
||||||
|
The administration interface your exchange must expose to let the system be bootstrapped before trading begins.
|
||||||
|
|
||||||
|
- `CreateInstrumentRequest/Response` — your exchange must accept requests to list a new tradeable instrument, create an order book for it, and return the assigned `order_book_id`.
|
||||||
|
|
||||||
|
User information is provided via a static JSON data file (see `users_data_schema.json` in the project root) whose path is supplied in each component's config as `dataFilePath`. Components that require authentication must read this file at startup to validate login credentials.
|
||||||
|
|
||||||
|
### `info.proto`
|
||||||
|
The market data feed your exchange must maintain and push to subscribed clients.
|
||||||
|
|
||||||
|
- `OnInstrument` — your exchange must push this to every client on connection (for all existing instruments) and again whenever a new instrument is created.
|
||||||
|
- `OrderBookSubscribeRequest/Response` — clients may subscribe to a specific instrument's order book in one of two modes. Your exchange must honour the chosen type and push updates accordingly:
|
||||||
|
- `TOP_OF_BOOK` — push `OnTopOfBook` whenever the best bid or ask changes.
|
||||||
|
- `PRICE_DEPTH_BOOK` — push `OnPriceDepthBook` with the full visible depth on every change.
|
||||||
|
- `OnTrade` — your exchange must broadcast this to all subscribed clients whenever a trade occurs, showing price, quantity, and which side was the aggressor.
|
||||||
|
|
||||||
|
### `order_book.proto`
|
||||||
|
The core of the exchange: the matching engine. This component stores resting orders, matches them when prices cross, and is the authoritative source of order book state.
|
||||||
|
|
||||||
|
It has two responsibilities:
|
||||||
|
|
||||||
|
**State broadcast** — pushed to all connected clients upon login (current state) and on every subsequent change:
|
||||||
|
- `OnOrderInserted` — broadcast when a new order enters the book, including any trades it immediately triggered.
|
||||||
|
- `OnOrderCancelled` — broadcast when an order is removed from the book.
|
||||||
|
- `OnTrade` — broadcast when a match occurs, carrying both the buy and sell `order_id`s and full trade details.
|
||||||
|
|
||||||
|
**Request/response** — your order book component must accept these from other internal services:
|
||||||
|
- `InsertOrderRequest/Response` — place a limit order identified by `order_book_id`. Your component must assign an `order_id`, record the timestamp, attempt to match, and return the result.
|
||||||
|
- `CancelOrderRequest/Response` — cancel a resting order by `order_book_id` and `order_id`. Your component must confirm the cancellation timestamp and remaining quantity at the time of removal.
|
||||||
|
|
||||||
|
### `execution.proto`
|
||||||
|
The client-facing execution interface your exchange must expose to trading clients. It is a higher-level entry point into the order book: clients address orders by `instrument_symbol` (human-readable) rather than `order_book_id` (internal numeric ID), so this component is responsible for the translation.
|
||||||
|
|
||||||
|
- `InsertOrderRequest/Response` — accept a limit order from a client, resolve the instrument to its order book, forward it, and return the result (order ID, timestamp, fills).
|
||||||
|
- `CancelOrderRequest/Response` — accept a cancellation request from a client and forward it to the appropriate order book.
|
||||||
|
- `OnTrade` — your exchange must send this **privately** to the specific client whose order was involved in a trade, reporting their `order_id`, side, and whether they were the aggressor.
|
||||||
|
|
||||||
|
### `risk_limits.proto`
|
||||||
|
The risk management component your exchange must implement to enforce guardrails on all trading activity. Before an order reaches the order book, risk limits must be checked.
|
||||||
|
|
||||||
|
Limits operate at two scopes:
|
||||||
|
|
||||||
|
**User-level** (`UserRiskLimits`):
|
||||||
|
- `max_outstanding_quantity` — caps the total quantity a user may have resting across all instruments simultaneously.
|
||||||
|
- `message_rate_rolling_limit` — caps the number of order/cancellation messages a user may send within a rolling time window.
|
||||||
|
|
||||||
|
**Instrument-level** (`InstrumentRiskLimits`):
|
||||||
|
- `max_outstanding_quantity` / `max_outstanding_amount` — caps how much of a single instrument a user may have resting at one time.
|
||||||
|
- `order_quantity_rolling_limit` / `order_amount_rolling_limit` — rolling-window caps on order flow per instrument.
|
||||||
|
|
||||||
|
Each scope has a symmetric `Get.../Set...` request/response pair. Your component must persist these limits and enforce them on every incoming order.
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange.admin;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
message CreateInstrumentRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
Instrument instrument = 2;
|
||||||
|
double tick_size = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateInstrumentResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
int64 created_timestamp = 3;
|
||||||
|
int64 order_book_id = 4;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange;
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
// Auth
|
||||||
|
AUTH_LOGIN_REQUEST = 0;
|
||||||
|
AUTH_LOGIN_RESPONSE = 1;
|
||||||
|
|
||||||
|
// Instrument admin
|
||||||
|
ADMIN_CREATE_INSTRUMENT_REQUEST = 20;
|
||||||
|
ADMIN_CREATE_INSTRUMENT_RESPONSE = 21;
|
||||||
|
|
||||||
|
// Info
|
||||||
|
INFO_ON_INSTRUMENT = 30;
|
||||||
|
INFO_SUBSCRIBE_REQUEST = 31;
|
||||||
|
INFO_SUBSCRIBE_RESPONSE = 32;
|
||||||
|
INFO_ON_TOP_OF_BOOK = 33;
|
||||||
|
INFO_ON_PRICE_DEPTH_BOOK = 34;
|
||||||
|
INFO_ON_TRADE = 35;
|
||||||
|
|
||||||
|
// Order Book
|
||||||
|
ORDER_BOOK_ON_ORDER_BOOK_CREATED = 40;
|
||||||
|
ORDER_BOOK_ON_ORDER_INSERTED = 41;
|
||||||
|
ORDER_BOOK_ON_ORDER_CANCELLED = 42;
|
||||||
|
ORDER_BOOK_ON_TRADE = 43;
|
||||||
|
ORDER_BOOK_GET_ALL_REQUEST = 44;
|
||||||
|
ORDER_BOOK_GET_ALL_RESPONSE = 45;
|
||||||
|
ORDER_BOOK_INSERT_ORDER_REQUEST = 46;
|
||||||
|
ORDER_BOOK_INSERT_ORDER_RESPONSE = 47;
|
||||||
|
ORDER_BOOK_CANCEL_ORDER_REQUEST = 48;
|
||||||
|
ORDER_BOOK_CANCEL_ORDER_RESPONSE = 49;
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
EXEC_INSERT_ORDER_REQUEST = 50;
|
||||||
|
EXEC_INSERT_ORDER_RESPONSE = 51;
|
||||||
|
EXEC_CANCEL_ORDER_REQUEST = 52;
|
||||||
|
EXEC_CANCEL_ORDER_RESPONSE = 53;
|
||||||
|
EXEC_ON_TRADE = 54;
|
||||||
|
|
||||||
|
// Risk
|
||||||
|
RISK_GET_USER_LIMITS_REQUEST = 60;
|
||||||
|
RISK_GET_USER_LIMITS_RESPONSE = 61;
|
||||||
|
RISK_SET_USER_LIMITS_REQUEST = 62;
|
||||||
|
RISK_SET_USER_LIMITS_RESPONSE = 63;
|
||||||
|
RISK_GET_INSTRUMENT_LIMITS_REQUEST = 64;
|
||||||
|
RISK_GET_INSTRUMENT_LIMITS_RESPONSE = 65;
|
||||||
|
RISK_SET_INSTRUMENT_LIMITS_REQUEST = 66;
|
||||||
|
RISK_SET_INSTRUMENT_LIMITS_RESPONSE = 67;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string username = 2;
|
||||||
|
string password = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Side {
|
||||||
|
BUY = 0;
|
||||||
|
SELL = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Instrument {
|
||||||
|
string symbol = 1;
|
||||||
|
string description = 2;
|
||||||
|
string currency = 3;
|
||||||
|
double multiplier = 4;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange.exec;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
message InsertOrderRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
Side side = 3;
|
||||||
|
double price = 4;
|
||||||
|
int32 quantity = 5;
|
||||||
|
// TODO start with LIMIT order type only
|
||||||
|
}
|
||||||
|
|
||||||
|
message InsertOrderResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
int64 order_id = 3;
|
||||||
|
int64 timestamp = 4;
|
||||||
|
repeated int64 trade_ids = 5;
|
||||||
|
int32 traded_quantity = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelOrderRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
int64 order_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelOrderResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnTrade {
|
||||||
|
int64 trade_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
int64 order_id = 4;
|
||||||
|
Side side = 5;
|
||||||
|
double price = 6;
|
||||||
|
int32 quantity = 7;
|
||||||
|
bool is_aggressive = 8;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange.info;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
// All active instruments are sent to client on connection, then any new instrument is sent on creation
|
||||||
|
message OnInstrument {
|
||||||
|
Instrument instrument = 1;
|
||||||
|
int64 created_timestamp = 2;
|
||||||
|
double tick_size = 3;
|
||||||
|
int64 order_book_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Subscription service to an instrument's order book
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
enum SubscriptionType {
|
||||||
|
TOP_OF_BOOK = 0;
|
||||||
|
PRICE_DEPTH_BOOK = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrderBookSubscribeRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
SubscriptionType subscription_type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrderBookSubscribeResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PriceLevel {
|
||||||
|
double price = 1;
|
||||||
|
int32 quantity = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnTopOfBook {
|
||||||
|
string instrument_symbol = 1;
|
||||||
|
int64 timestamp = 2;
|
||||||
|
PriceLevel best_bid = 3;
|
||||||
|
PriceLevel best_ask = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnPriceDepthBook {
|
||||||
|
string instrument_symbol = 1;
|
||||||
|
int64 timestamp = 2;
|
||||||
|
repeated PriceLevel bids = 3;
|
||||||
|
repeated PriceLevel asks = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnTrade {
|
||||||
|
int64 trade_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
double price = 4;
|
||||||
|
int32 quantity = 5;
|
||||||
|
Side aggressor_side = 6;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange.orderbook;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Messages sent to all clients upon logging in with the current
|
||||||
|
// state of the order book, then upon any changes
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
message OnOrderBookCreated {
|
||||||
|
int64 order_book_id = 1;
|
||||||
|
double tick_size = 2;
|
||||||
|
int64 created_timestamp = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnOrderInserted {
|
||||||
|
int64 order_id = 1;
|
||||||
|
int64 order_book_id = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
Side side = 4;
|
||||||
|
double price = 5;
|
||||||
|
int32 quantity = 6;
|
||||||
|
string username = 7;
|
||||||
|
|
||||||
|
repeated int64 trade_ids = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnOrderCancelled {
|
||||||
|
int64 order_id = 1;
|
||||||
|
int64 cancellation_timestamp = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OnTrade {
|
||||||
|
int64 trade_id = 1;
|
||||||
|
int64 order_book_id = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
int64 buy_order_id = 4;
|
||||||
|
int64 sell_order_id = 5;
|
||||||
|
double price = 6;
|
||||||
|
int32 quantity = 7;
|
||||||
|
Side aggressor_side = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Requests accepted by this service
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
message InsertOrderRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
int64 order_book_id = 2;
|
||||||
|
Side side = 3;
|
||||||
|
double price = 4;
|
||||||
|
int32 quantity = 5;
|
||||||
|
string username = 6;
|
||||||
|
// TODO start with LIMIT order type only
|
||||||
|
}
|
||||||
|
|
||||||
|
message InsertOrderResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
int64 order_id = 3;
|
||||||
|
int64 timestamp = 4;
|
||||||
|
repeated int64 trade_ids = 5;
|
||||||
|
int32 traded_quantity = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelOrderRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
int64 order_book_id = 2;
|
||||||
|
int64 order_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CancelOrderResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
int64 cancellation_timestamp = 3;
|
||||||
|
int32 remaining_quantity = 4;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package optiver.exchange.risk_limits;
|
||||||
|
|
||||||
|
message RollingWindowLimit {
|
||||||
|
int32 limit = 1;
|
||||||
|
int32 window_in_seconds = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserRiskLimits {
|
||||||
|
int64 max_outstanding_quantity = 1;
|
||||||
|
RollingWindowLimit message_rate_rolling_limit = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InstrumentRiskLimits {
|
||||||
|
int64 max_outstanding_quantity = 1;
|
||||||
|
double max_outstanding_amount = 2;
|
||||||
|
RollingWindowLimit order_quantity_rolling_limit = 3;
|
||||||
|
RollingWindowLimit order_amount_rolling_limit = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Requests accepted by this service
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
message GetUserRiskLimitsRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserRiskLimitsResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
UserRiskLimits user_risk_limits = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetUserRiskLimitsRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
UserRiskLimits user_risk_limits = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetUserRiskLimitsResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInstrumentRiskLimitsRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetInstrumentRiskLimitsResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
|
||||||
|
map<string, InstrumentRiskLimits> risk_limits_by_instrument = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetInstrumentRiskLimitsRequest {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string instrument_symbol = 2;
|
||||||
|
InstrumentRiskLimits instrument_risk_limits = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetInstrumentRiskLimitsResponse {
|
||||||
|
int64 request_id = 1;
|
||||||
|
string error_message = 2;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
[project]
|
||||||
|
name = "Optivex"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Career Kickstarter - Optiver's Exchange"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"flake8>=7.3.0",
|
||||||
|
"flake8-mypy>=17.8.0",
|
||||||
|
"jsonschema>=4.26.0",
|
||||||
|
"mypy>=1.19.1",
|
||||||
|
"mypy-protobuf>=5.0.0",
|
||||||
|
"protobuf>=7.34.0",
|
||||||
|
"pytest>=9.0.2",
|
||||||
|
"types-jsonschema>=4.23.0.20241208",
|
||||||
|
"types-protobuf>=6.32.1.20260221",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
sample-app = "sample_app.main:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel", "uv"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"application" = ["*.json"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
package = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
exclude = "src/proto"
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
from types import FrameType
|
||||||
|
from typing import Any, Union
|
||||||
|
from importlib.resources.abc import Traversable
|
||||||
|
import jsonschema
|
||||||
|
import json
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseApplication(ABC):
|
||||||
|
def __init__(self, config_schema: Union[Path, Traversable], app_name: str | None = None) -> None:
|
||||||
|
self._executable_name = Path(sys.argv[0]).absolute().name
|
||||||
|
self._app_name = app_name or self._executable_name
|
||||||
|
self._config_schema = _load_json(config_schema)
|
||||||
|
|
||||||
|
self._init_args_parser()
|
||||||
|
self._args = self._parser.parse_args()
|
||||||
|
self._config_file = self._find_config_file_path()
|
||||||
|
self._config = _load_json(self._config_file)
|
||||||
|
self._validate_config()
|
||||||
|
|
||||||
|
self._init_logging()
|
||||||
|
logger.info(f"Config loaded: {json.dumps(self._config, indent=4)}")
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
"""Call this method to run the application."""
|
||||||
|
self._register_signal_handlers()
|
||||||
|
try:
|
||||||
|
logger.info("Starting application...")
|
||||||
|
self._start()
|
||||||
|
logger.info("Normal application exit, shutting down")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Oops, something went wrong.")
|
||||||
|
logger.error("Shutting down")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _start(self) -> None:
|
||||||
|
"""This method should be implemented by the subclass to start the application."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _init_args_parser(self) -> None:
|
||||||
|
self._parser = argparse.ArgumentParser()
|
||||||
|
self._parser.add_argument("-c", "--config", help="(String) Path to config file to load", default=None)
|
||||||
|
|
||||||
|
def _find_config_file_path(self) -> Path:
|
||||||
|
if self._args.config is None:
|
||||||
|
default_config_file = Path(sys.argv[0]).parent / f"{self._app_name}_config.json"
|
||||||
|
logger.info(f"No config file specified, using default: {default_config_file}")
|
||||||
|
return default_config_file
|
||||||
|
return Path(self._args.config)
|
||||||
|
|
||||||
|
def _validate_config(self) -> None:
|
||||||
|
try:
|
||||||
|
jsonschema.validate(instance=self._config, schema=self._config_schema)
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
raise SystemExit(f"Failed to validate config: {e}")
|
||||||
|
|
||||||
|
def _init_logging(self) -> None:
|
||||||
|
log_level_str = self._config["logLevel"]
|
||||||
|
log_directory = Path(self._config["logDirectory"])
|
||||||
|
log_file = log_directory / f"{self._app_name}_{datetime.now():%Y%m%d_%H%M%S}.log"
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
level=_get_log_level(log_level_str),
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler(log_file),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger.info(f"Logging initialized. Log file: {log_file}")
|
||||||
|
|
||||||
|
def _register_signal_handlers(self) -> None:
|
||||||
|
signal.signal(signal.SIGINT, handler=self._shutdown)
|
||||||
|
signal.signal(signal.SIGTERM, handler=self._shutdown)
|
||||||
|
|
||||||
|
def _shutdown(self, signum: int, frame: FrameType | None) -> None:
|
||||||
|
logger.info(f"Received signal {signal.Signals(signum).name} ({signum}) at frame {frame}, shutting down")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_log_level(log_level_str: str) -> Any:
|
||||||
|
logging_level = logging.getLevelNamesMapping().get(log_level_str)
|
||||||
|
if logging_level is None:
|
||||||
|
raise ValueError(f"Invalid log level: {log_level_str}")
|
||||||
|
return logging_level
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(file: Union[Path, Traversable]) -> dict:
|
||||||
|
logger.debug(f"Loading JSON file: {file}")
|
||||||
|
with file.open("r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "/optiver/application_config.json",
|
||||||
|
"title": "ApplicationConfig",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"logLevel": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["DEBUG", "INFO", "WARN", "ERROR"],
|
||||||
|
"description": "The level of logging for the application."
|
||||||
|
},
|
||||||
|
"logDirectory": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"description": "The directory where log files will be stored."
|
||||||
|
},
|
||||||
|
"listenOn": {
|
||||||
|
"$ref": "#/$defs/ConnectionConfig"
|
||||||
|
},
|
||||||
|
"connectTo": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^.*$": {
|
||||||
|
"$ref": "#/$defs/ConnectionConfig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dataFilePath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to the JSON data file with user data. Schema of this file is defined in data_file_schema.json."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["logLevel", "logDirectory", "listenOn", "dataFilePath"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"ConnectionConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"host": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "hostname",
|
||||||
|
"description": "The hostname or IP address of the server."
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535,
|
||||||
|
"description": "The port number the server will listen on."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["host", "port"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "/optiver/data_file.json",
|
||||||
|
"title": "DataFile",
|
||||||
|
"description": "Static data file containing pre-registered users for the exchange.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"users": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "List of users who can authenticate with the exchange.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["users"],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$defs": {
|
||||||
|
"User": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Unique username for authentication."
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Password for authentication."
|
||||||
|
},
|
||||||
|
"full_name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Full display name of the user."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["username", "password", "full_name"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Callable, Generic, Type, TypeVar
|
||||||
|
|
||||||
|
from connection import message_codec
|
||||||
|
from connection.ip_address import IpAddress
|
||||||
|
from google.protobuf.message import Message
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ProtoMessage = TypeVar('ProtoMessage', bound=Message)
|
||||||
|
|
||||||
|
class ConnectionHandler(ABC):
|
||||||
|
def __init__(self, socket_fd: socket.socket, ip_address: IpAddress,
|
||||||
|
close_callback: Callable[[], None]) -> None:
|
||||||
|
self.socket_fd = socket_fd
|
||||||
|
self.ip_address = ip_address
|
||||||
|
self.close_callback = close_callback
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||||
|
logger.info(f"Closing connection to {self.ip_address}...")
|
||||||
|
self.close_callback() # TODO graceful flag to avoid reentrance?
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle_message(self, message_type: int, message: bytes) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_disconnect(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_message(self, message_type: int, message: ProtoMessage) -> None:
|
||||||
|
logger.info(f"Preparing to send message of type {message_type} to {self.ip_address}...")
|
||||||
|
logger.debug(f"Message: {message}")
|
||||||
|
try:
|
||||||
|
serialized_message = message.SerializeToString()
|
||||||
|
encoded_message = message_codec.encode_message(message_type, serialized_message)
|
||||||
|
self._send_message(encoded_message)
|
||||||
|
logger.info(f"Sent message successfully")
|
||||||
|
except socket.error as e:
|
||||||
|
logger.exception(f"Failed to send message to {self.ip_address}")
|
||||||
|
logger.info(f"Closing connection to {self.ip_address}...")
|
||||||
|
self.close_callback()
|
||||||
|
|
||||||
|
def _send_message(self, encoded_message: bytes) -> None:
|
||||||
|
logger.debug(f"Sending message of {len(encoded_message)} bytes")
|
||||||
|
self.socket_fd.sendall(encoded_message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deserialize_message(proto_message_type: type[ProtoMessage], message: bytes) -> ProtoMessage:
|
||||||
|
proto_message = proto_message_type()
|
||||||
|
proto_message.ParseFromString(message)
|
||||||
|
logger.debug(f"Deserialized message of type {proto_message_type.__name__}: {proto_message}")
|
||||||
|
return proto_message
|
||||||
|
|
||||||
|
|
||||||
|
ConnectionHandlerType = TypeVar('ConnectionHandlerType', bound=ConnectionHandler)
|
||||||
|
|
||||||
|
class ConnectionHandlerFactory(ABC, Generic[ConnectionHandlerType]):
|
||||||
|
@abstractmethod
|
||||||
|
def on_new_connection(self, socket_fd: socket.socket, ip_address: IpAddress,
|
||||||
|
close_callback: Callable[[], None]) -> ConnectionHandlerType:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_connection_closed(self, connection_handler: ConnectionHandlerType) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LambdaConnectionHandlerFactory(ConnectionHandlerFactory[ConnectionHandlerType]):
|
||||||
|
def __init__(self, on_new_connection_lambda: Callable[[socket.socket, IpAddress, Callable[[], None]], ConnectionHandlerType]) -> None:
|
||||||
|
self._on_new_connection_lambda = on_new_connection_lambda
|
||||||
|
|
||||||
|
def on_new_connection(self, socket_fd: socket.socket, ip_address: IpAddress, close_callback: Callable[[], None]) -> ConnectionHandlerType:
|
||||||
|
return self._on_new_connection_lambda(socket_fd, ip_address, close_callback)
|
||||||
|
|
||||||
|
def on_connection_closed(self, connection_handler: ConnectionHandlerType) -> None:
|
||||||
|
# No-op by default; can be extended if needed
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IpAddress:
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.host}:{self.port}"
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
BYTE_ORDER: Literal['little', 'big'] = 'big'
|
||||||
|
MESSAGE_SIZE_BYTES = 4
|
||||||
|
MESSAGE_TYPE_BYTES = 4
|
||||||
|
|
||||||
|
|
||||||
|
def encode_message(message_type: int, message: bytes) -> bytes:
|
||||||
|
message_size: int = len(message) + MESSAGE_TYPE_BYTES
|
||||||
|
output_stream: bytes = message_size.to_bytes(MESSAGE_SIZE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
output_stream += message_type.to_bytes(MESSAGE_TYPE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
output_stream += message
|
||||||
|
return output_stream
|
||||||
|
|
||||||
|
|
||||||
|
def read_message(socket_fd: socket.socket) -> tuple[int, bytes]:
|
||||||
|
raw_msg_len = socket_fd.recv(MESSAGE_SIZE_BYTES)
|
||||||
|
if not raw_msg_len:
|
||||||
|
raise BrokenPipeError("No data on socket")
|
||||||
|
|
||||||
|
msg_len = int.from_bytes(raw_msg_len, byteorder=BYTE_ORDER)
|
||||||
|
logger.debug(f"Received expected message length: {msg_len}")
|
||||||
|
|
||||||
|
raw_msg = socket_fd.recv(msg_len)
|
||||||
|
logger.debug(f"Actual message length: {len(raw_msg)}")
|
||||||
|
|
||||||
|
message_type = int.from_bytes(raw_msg[:MESSAGE_TYPE_BYTES], byteorder=BYTE_ORDER)
|
||||||
|
message = raw_msg[MESSAGE_TYPE_BYTES:]
|
||||||
|
return message_type, message
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import socket
|
||||||
|
import selectors
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
from connection import message_codec
|
||||||
|
from connection.connection_handler import ConnectionHandler, ConnectionHandlerFactory, ConnectionHandlerType
|
||||||
|
from connection.ip_address import IpAddress
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
NO_TIMEOUT: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class _TcpServerContextManager:
|
||||||
|
def __init__(self, close_callback: Callable[[], None]) -> None:
|
||||||
|
self.close_callback = close_callback
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||||
|
self.close_callback()
|
||||||
|
|
||||||
|
|
||||||
|
class _ConnectionType(Enum):
|
||||||
|
SERVER = 1
|
||||||
|
CLIENT = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _ConnectionData:
|
||||||
|
connection_type: _ConnectionType
|
||||||
|
handler_factory: ConnectionHandlerFactory
|
||||||
|
handler: ConnectionHandler | None = None # Only used for client connections
|
||||||
|
|
||||||
|
|
||||||
|
_LambdaConnectionHandlerFactory = Callable[[socket.socket, IpAddress, Callable[[], None]], ConnectionHandlerType]
|
||||||
|
|
||||||
|
|
||||||
|
class _LambdaConnectionHandlerFactoryWrapper(ConnectionHandlerFactory[ConnectionHandlerType]):
|
||||||
|
def __init__(self, factory: _LambdaConnectionHandlerFactory) -> None:
|
||||||
|
self.factory = factory
|
||||||
|
|
||||||
|
def on_new_connection(self, socket_fd: socket.socket, ip_address: IpAddress,
|
||||||
|
close_callback: Callable[[], None]) -> ConnectionHandlerType:
|
||||||
|
return self.factory(socket_fd, ip_address, close_callback)
|
||||||
|
|
||||||
|
def on_connection_closed(self, connection_handler: ConnectionHandlerType) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TcpConnectionManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.socket_selector = selectors.DefaultSelector()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||||
|
self.socket_selector.close()
|
||||||
|
# TODO is this the right way to close the connection?
|
||||||
|
# for connection_handler in self.connection_map.values():
|
||||||
|
# connection_handler.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
|
def listen(self, ip_address: IpAddress, handler_factory: ConnectionHandlerFactory | _LambdaConnectionHandlerFactory) -> _TcpServerContextManager:
|
||||||
|
"""
|
||||||
|
Starts a non-blocking TCP/IP server on the specified host and port.
|
||||||
|
"""
|
||||||
|
if callable(handler_factory):
|
||||||
|
handler_factory = _LambdaConnectionHandlerFactoryWrapper(handler_factory)
|
||||||
|
|
||||||
|
logger.info(f"Starting server on {ip_address}")
|
||||||
|
server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||||
|
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Allow address reuse
|
||||||
|
server_socket.bind((ip_address.host, ip_address.port))
|
||||||
|
server_socket.listen()
|
||||||
|
server_socket.setblocking(False)
|
||||||
|
logger.info(f'Listening on {ip_address}')
|
||||||
|
|
||||||
|
connection_data = _ConnectionData(_ConnectionType.SERVER, handler_factory)
|
||||||
|
self.socket_selector.register(server_socket, selectors.EVENT_READ, data=connection_data)
|
||||||
|
logger.info(f"Server started")
|
||||||
|
|
||||||
|
close_callback = lambda: self._close_socket(server_socket, ip_address)
|
||||||
|
return _TcpServerContextManager(close_callback)
|
||||||
|
|
||||||
|
def connect(self, ip_address: IpAddress,
|
||||||
|
handler_factory: ConnectionHandlerFactory[ConnectionHandlerType] | _LambdaConnectionHandlerFactory) -> ConnectionHandlerType:
|
||||||
|
if callable(handler_factory):
|
||||||
|
handler_factory = _LambdaConnectionHandlerFactoryWrapper(handler_factory)
|
||||||
|
|
||||||
|
logger.info(f"Connecting to {ip_address}")
|
||||||
|
conn_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
|
||||||
|
result = conn_socket.connect_ex((ip_address.host, ip_address.port))
|
||||||
|
if result != 0:
|
||||||
|
raise ConnectionError(f"Failed to connect to {ip_address}: {errno.errorcode[result]}")
|
||||||
|
|
||||||
|
logger.info(f"Connected to {ip_address}")
|
||||||
|
connection_handler = self._on_new_connection(conn_socket, ip_address, handler_factory)
|
||||||
|
return connection_handler
|
||||||
|
|
||||||
|
def wait_for_events(self, timeout_in_seconds: float | None = NO_TIMEOUT) -> int:
|
||||||
|
"""
|
||||||
|
Check for events on the server socket and client sockets.
|
||||||
|
Call this method in a loop to keep the server running.
|
||||||
|
@param timeout_in_seconds: The time in seconds to wait for events before returning. Zero means non-blocking.
|
||||||
|
@return: The number of events that occurred.
|
||||||
|
"""
|
||||||
|
logger.debug(f"Checking for socket events with timeout {timeout_in_seconds}")
|
||||||
|
events = self.socket_selector.select(timeout=timeout_in_seconds)
|
||||||
|
logger.debug(f"Received {len(events)} events")
|
||||||
|
for key, mask in events:
|
||||||
|
assert isinstance(key.data, _ConnectionData)
|
||||||
|
connection_data = key.data
|
||||||
|
if connection_data.connection_type == _ConnectionType.SERVER:
|
||||||
|
assert isinstance(key.fileobj, socket.socket)
|
||||||
|
assert connection_data.handler_factory is not None
|
||||||
|
self._accept_client(key.fileobj, connection_data.handler_factory)
|
||||||
|
elif mask & selectors.EVENT_READ:
|
||||||
|
self._read_from_socket(key)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected event mask {mask}")
|
||||||
|
logger.debug(f"Done checking for socket events")
|
||||||
|
return len(events)
|
||||||
|
|
||||||
|
def _accept_client(self, socket_fd: socket.socket, handler_factory: ConnectionHandlerFactory) -> None:
|
||||||
|
client_socket, address_info = socket_fd.accept()
|
||||||
|
logger.info(f"Accepted connection from {address_info}")
|
||||||
|
|
||||||
|
assert isinstance(address_info, tuple) and len(address_info) == 2
|
||||||
|
ip_address = IpAddress(host=address_info[0], port=address_info[1])
|
||||||
|
self._on_new_connection(client_socket, ip_address, handler_factory)
|
||||||
|
|
||||||
|
def _on_new_connection(self, client_socket_fd: socket.socket, ip_address: IpAddress,
|
||||||
|
handler_factory: ConnectionHandlerFactory[ConnectionHandlerType]) -> ConnectionHandlerType:
|
||||||
|
logger.debug(f"Setting up client connection with {ip_address}")
|
||||||
|
client_socket_fd.setblocking(False)
|
||||||
|
|
||||||
|
close_callback = lambda: self._close_socket(client_socket_fd, ip_address)
|
||||||
|
client_connection = handler_factory.on_new_connection(client_socket_fd, ip_address, close_callback)
|
||||||
|
|
||||||
|
connection_data = _ConnectionData(_ConnectionType.CLIENT, handler_factory, handler=client_connection)
|
||||||
|
self.socket_selector.register(client_socket_fd, selectors.EVENT_READ, data=connection_data)
|
||||||
|
|
||||||
|
logger.debug(f"Done setting up client connection with {ip_address}")
|
||||||
|
return client_connection
|
||||||
|
|
||||||
|
def _read_from_socket(self, key: selectors.SelectorKey) -> None:
|
||||||
|
socket_fd: socket.socket = key.fileobj # type: ignore
|
||||||
|
connection_data: _ConnectionData = key.data
|
||||||
|
assert connection_data.connection_type == _ConnectionType.CLIENT
|
||||||
|
assert connection_data.handler is not None
|
||||||
|
client_connection = connection_data.handler
|
||||||
|
ip_address = client_connection.ip_address
|
||||||
|
logger.debug(f"Reading from socket of {ip_address}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
message_type, message = message_codec.read_message(socket_fd)
|
||||||
|
logger.debug(f"Received message type {message_type} with length {len(message)}")
|
||||||
|
except (BrokenPipeError, ConnectionResetError) as e:
|
||||||
|
logger.warning(f"Error while reading from {ip_address}: {str(e)}. Client will be disconnected")
|
||||||
|
self._close_socket(socket_fd, ip_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client_connection.handle_message(message_type, message)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(f"Error while handling message from {ip_address}. Client will be disconnected")
|
||||||
|
self._close_socket(socket_fd, ip_address)
|
||||||
|
raise
|
||||||
|
logger.debug(f"Done handling message")
|
||||||
|
|
||||||
|
def _close_socket(self, socket_fd: socket.socket, ip_address: IpAddress) -> None:
|
||||||
|
logger.debug(f"Closing socket on {ip_address}...")
|
||||||
|
connection_data: _ConnectionData = self.socket_selector.get_key(socket_fd).data
|
||||||
|
self.socket_selector.unregister(socket_fd)
|
||||||
|
socket_fd.close()
|
||||||
|
logger.info(f"Socket on {ip_address} closed")
|
||||||
|
|
||||||
|
if connection_data.connection_type == _ConnectionType.CLIENT:
|
||||||
|
assert connection_data.handler is not None
|
||||||
|
client_connection = connection_data.handler
|
||||||
|
client_connection.on_disconnect()
|
||||||
|
connection_data.handler_factory.on_connection_closed(client_connection)
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Sample App
|
||||||
|
|
||||||
|
This is a sample application for the Stock Exchange training program.
|
||||||
|
|
||||||
|
## Quick Tip: Connecting to the Server via Command Line (Linux)
|
||||||
|
|
||||||
|
1. **Connect to the Server**
|
||||||
|
Use `telnet` to connect to the server:
|
||||||
|
```bash
|
||||||
|
telnet <server_address> <port>
|
||||||
|
```
|
||||||
|
Replace `<server_address>` and `<port>` with the appropriate values.
|
||||||
|
|
||||||
|
2. **Send Messages**
|
||||||
|
Once connected, type your message and press `Enter` to send it to the server.
|
||||||
|
|
||||||
|
3. **View Responses**
|
||||||
|
The server will echo back the messages you send.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
telnet localhost 8080
|
||||||
|
Hello, Server!
|
||||||
|
# Server response
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
from typing import Callable
|
||||||
|
from connection import message_codec
|
||||||
|
from connection.connection_handler import ConnectionHandler, ConnectionHandlerFactory
|
||||||
|
from connection.ip_address import IpAddress
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PingPongClientHandler(ConnectionHandler):
|
||||||
|
def on_disconnect(self) -> None:
|
||||||
|
logger.info(f"Client {self.ip_address} disconnected")
|
||||||
|
|
||||||
|
def handle_message(self, message_type: int, message: bytes) -> None:
|
||||||
|
logger.info(f"Received message of type {message_type}")
|
||||||
|
logger.debug(f"Message: {str(message)}")
|
||||||
|
|
||||||
|
logger.info("Bouncing message back to client")
|
||||||
|
encoded_message = message_codec.encode_message(message_type, message)
|
||||||
|
self._send_message(encoded_message)
|
||||||
|
logger.info("Message bounced back")
|
||||||
|
|
||||||
|
|
||||||
|
class PingPongClientHandlerFactory(ConnectionHandlerFactory[PingPongClientHandler]):
|
||||||
|
def on_new_connection(self, socket_fd: socket.socket, ip_address: IpAddress,
|
||||||
|
close_callback: Callable[[], None]) -> PingPongClientHandler:
|
||||||
|
return PingPongClientHandler(socket_fd, ip_address, close_callback)
|
||||||
|
|
||||||
|
def on_connection_closed(self, connection_handler: PingPongClientHandler):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from application.application import BaseApplication
|
||||||
|
from connection.ip_address import IpAddress
|
||||||
|
from connection.tcp_connection_manager import TcpConnectionManager
|
||||||
|
from sample_app.connection_handler import PingPongClientHandlerFactory
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SampleApplication(BaseApplication):
|
||||||
|
def _start(self) -> None:
|
||||||
|
logger.info("Starting the sample application...")
|
||||||
|
connection_handler_factory = PingPongClientHandlerFactory()
|
||||||
|
tcp_connection_manager = TcpConnectionManager()
|
||||||
|
|
||||||
|
server_ip_address = IpAddress(
|
||||||
|
host=self._config["listenOn"]["host"],
|
||||||
|
port=self._config["listenOn"]["port"])
|
||||||
|
logger.info(f"Starting server on {server_ip_address}")
|
||||||
|
with tcp_connection_manager.listen(server_ip_address, connection_handler_factory):
|
||||||
|
logger.info("Server started.")
|
||||||
|
logger.info("Running event loop until interrupted.")
|
||||||
|
while True:
|
||||||
|
tcp_connection_manager.wait_for_events()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
config_schema_path = Path(__file__).parent.parent / "application" / "config_schema.json"
|
||||||
|
app = SampleApplication(config_schema=config_schema_path, app_name="sample_app")
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"logLevel": "DEBUG",
|
||||||
|
"logDirectory": "./logs",
|
||||||
|
"listenOn": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 51301
|
||||||
|
},
|
||||||
|
"connectTo": {
|
||||||
|
"client1": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 51302
|
||||||
|
},
|
||||||
|
"client2": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 51303
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
from connection.message_codec import BYTE_ORDER, MESSAGE_SIZE_BYTES, MESSAGE_TYPE_BYTES, encode_message
|
||||||
|
|
||||||
|
|
||||||
|
class TestEncodeMessage:
|
||||||
|
def test_encodes_empty_payload(self):
|
||||||
|
result = encode_message(message_type=1, message=b"")
|
||||||
|
|
||||||
|
expected_size = MESSAGE_TYPE_BYTES.to_bytes(MESSAGE_SIZE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
expected_type = (1).to_bytes(MESSAGE_TYPE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
assert result == expected_size + expected_type
|
||||||
|
|
||||||
|
def test_encodes_payload(self):
|
||||||
|
payload = b"\x0a\x0b\x0c"
|
||||||
|
result = encode_message(message_type=42, message=payload)
|
||||||
|
|
||||||
|
expected_size = (MESSAGE_TYPE_BYTES + len(payload)).to_bytes(MESSAGE_SIZE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
expected_type = (42).to_bytes(MESSAGE_TYPE_BYTES, byteorder=BYTE_ORDER)
|
||||||
|
assert result == expected_size + expected_type + payload
|
||||||
|
|
||||||
|
def test_size_field_excludes_itself(self):
|
||||||
|
payload = b"\xff"
|
||||||
|
result = encode_message(message_type=0, message=payload)
|
||||||
|
|
||||||
|
size_field = int.from_bytes(result[:MESSAGE_SIZE_BYTES], byteorder=BYTE_ORDER)
|
||||||
|
assert size_field == MESSAGE_TYPE_BYTES + len(payload)
|
||||||
|
|
@ -0,0 +1,494 @@
|
||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "25.3.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "7.3.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mccabe" },
|
||||||
|
{ name = "pycodestyle" },
|
||||||
|
{ name = "pyflakes" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8-mypy"
|
||||||
|
version = "17.8.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "flake8" },
|
||||||
|
{ name = "mypy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/97/9a/cddd1363d7314bb4eb452089c6fb3092ed9fda9f3350683d1978522a30ec/flake8-mypy-17.8.0.tar.gz", hash = "sha256:47120db63aff631ee1f84bac6fe8e64731dc66da3efc1c51f85e15ade4a3ba18" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a8/5d/b172de53870fc6fa2336a6f55dca0d67a70e2bf31218ed160aea3239e644/flake8_mypy-17.8.0-py35.py36-none-any.whl", hash = "sha256:cff009f4250e8391bf48990093cff85802778c345c8449d6498b62efefeebcbc" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema"
|
||||||
|
version = "4.26.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "jsonschema-specifications" },
|
||||||
|
{ name = "referencing" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema-specifications"
|
||||||
|
version = "2024.10.1"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "referencing" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "librt"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "1.19.1"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-protobuf"
|
||||||
|
version = "5.0.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "protobuf" },
|
||||||
|
{ name = "types-protobuf" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d5/48/658827446368bca30a94e545598065587ece9cd09b678d7d2895c37a59d2/mypy_protobuf-5.0.0.tar.gz", hash = "sha256:6fdd1cfdbb4419c713291d800a332d4bba6510dbd1341ed95e0bcc82fcadb6b5" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b4/b1/ab1e7a49930a8c1d1f7a570bbd4ec7d552ef035acc7aa4b97906e17a34a9/mypy_protobuf-5.0.0-py3-none-any.whl", hash = "sha256:3a7dd753ef3e3b8783a824eb51f07983f62812f9ec066e4fbb1b22d6c5dc36d0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "optivex"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flake8" },
|
||||||
|
{ name = "flake8-mypy" },
|
||||||
|
{ name = "jsonschema" },
|
||||||
|
{ name = "mypy" },
|
||||||
|
{ name = "mypy-protobuf" },
|
||||||
|
{ name = "protobuf" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "types-jsonschema" },
|
||||||
|
{ name = "types-protobuf" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "flake8", specifier = ">=7.3.0" },
|
||||||
|
{ name = "flake8-mypy", specifier = ">=17.8.0" },
|
||||||
|
{ name = "jsonschema", specifier = ">=4.26.0" },
|
||||||
|
{ name = "mypy", specifier = ">=1.19.1" },
|
||||||
|
{ name = "mypy-protobuf", specifier = ">=5.0.0" },
|
||||||
|
{ name = "protobuf", specifier = ">=7.34.0" },
|
||||||
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
|
{ name = "types-jsonschema", specifier = ">=4.23.0.20241208" },
|
||||||
|
{ name = "types-protobuf", specifier = ">=6.32.1.20260221" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "24.2"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protobuf"
|
||||||
|
version = "7.34.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f2/00/04a2ab36b70a52d0356852979e08b44edde0435f2115dc66e25f2100f3ab/protobuf-7.34.0.tar.gz", hash = "sha256:3871a3df67c710aaf7bb8d214cc997342e63ceebd940c8c7fc65c9b3d697591a" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/45/99/b029bbbc61e8937545da5b79aa405ab2d9cf307a728f8c9459ad60d7a481/protobuf-7.34.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:9d7a5005fb96f3c1e64f397f91500b0eb371b28da81296ae73a6b08a5b76cdd6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/cc/79/09f02671eb75b251c5550a1c48e7b3d4b0623efd7c95a15a50f6f9fc1e2e/protobuf-7.34.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4a72a8ec94e7a9f7ef7fe818ed26d073305f347f8b3b5ba31e22f81fd85fca02" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/1f/3e/38ff2ddee5cc946f575c9d8cc822e34bde205cf61acf8099ad88ef19d7d2/protobuf-7.34.0-cp310-abi3-win32.whl", hash = "sha256:f791ec509707a1d91bd02e07df157e75e4fb9fbdad12a81b7396201ec244e2e3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/cb/71/7c32eaf34a61a1bae1b62a2ac4ffe09b8d1bb0cf93ad505f42040023db89/protobuf-7.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:9f9079f1dde4e32342ecbd1c118d76367090d4aaa19da78230c38101c5b3dd40" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl", hash = "sha256:e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "referencing"
|
||||||
|
version = "0.36.2"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "rpds-py" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpds-py"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4" },
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-jsonschema"
|
||||||
|
version = "4.23.0.20241208"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "referencing" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/ad/e6/9e5cd771687086844caa43dbb211ec0d1cfa899d17c110f3220efcd46e83/types_jsonschema-4.23.0.20241208.tar.gz", hash = "sha256:e8b15ad01f290ecf6aea53f93fbdf7d4730e4600313e89e8a7f95622f7e87b7c" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/91/64/4b2fba8b7cb0104ba013f2a1bf6f39a98e927e14befe1ef947d373b25218/types_jsonschema-4.23.0.20241208-py3-none-any.whl", hash = "sha256:87934bd9231c99d8eff94cacfc06ba668f7973577a9bd9e1f9de957c5737313e" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-protobuf"
|
||||||
|
version = "6.32.1.20260221"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/5f/e2/9aa4a3b2469508bd7b4e2ae11cbedaf419222a09a1b94daffcd5efca4023/types_protobuf-6.32.1.20260221.tar.gz", hash = "sha256:6d5fb060a616bfb076cbb61b4b3c3969f5fc8bec5810f9a2f7e648ee5cbcbf6e" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/2e/e8/1fd38926f9cf031188fbc5a96694203ea6f24b0e34bd64a225ec6f6291ba/types_protobuf-6.32.1.20260221-py3-none-any.whl", hash = "sha256:da7cdd947975964a93c30bfbcc2c6841ee646b318d3816b033adc2c4eb6448e4" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.13.0"
|
||||||
|
source = { registry = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/simple" }
|
||||||
|
sdist = { url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://optiver.jfrog.io/artifactory/api/pypi/pypi/packages/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5" },
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue