diff --git a/README.md b/README.md index 6f64f789..6dcc0ff8 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ pip install -r requirements.txt * [`worker`](https://github.com/delftdata/styx/tree/main/worker) Styx worker. +* [`obol`](https://github.com/delftdata/styx/tree/main/obol) + The Obol source-to-source compiler that turns sequential Python into Styx operator functions. + ## Container images The coordinator and worker images are published to the GitHub Container Registry (GHCR). @@ -180,6 +183,35 @@ with the Kafka, coordinator, and S3-compatible storage environment variables. To clear the SE: `docker compose down --volumes` +## Obol: write Styx programs as ordinary sequential Python + +[`obol`](obol/) is a source-to-source compiler that lets you write distributed +stateful workflows as plain, type-annotated, object-oriented Python and compiles +them into the asynchronous, message-passing operator functions that the Styx +runtime expects. + +Instead of hand-decomposing a method into a chain of callbacks, you write +entities and call their methods, and Obol generates the routing, state +persistence, and continuation management. Compiled programs inherit Styx's +exactly-once guarantees. + +- **Entities** are classes annotated with `@entity`; their `__init__` attributes + are their persistent state (essentially the database schema), and `__key__()` + returns the routing key Styx partitions on. +- **Methods** are plain synchronous Python. A call on a value typed as an + `@entity` compiles to a remote dispatch; everything else stays local. +- **Concurrency** is expressed with two constructs: `send_async` + (fire-and-forget) and `gather` (fan-out/fan-in), the latter compiling to a + failure-durable synchronization barrier. + +Under the hood, Obol is a multi-stage pipeline over the libcst syntax tree +(syntactic preparation, `mypy`-based type resolution, live-variable analysis, +and CPS-style function splitting at every remote-call boundary). Every +cross-entity call in the source compiles to exactly one asynchronous dispatch in +the output, with no extra round-trips introduced. + +See the [`obol/` README](obol/README.md) for more. + ##### Cite Styx ```bibtex diff --git a/docker-compose-combined.yml b/docker-compose-combined.yml new file mode 100644 index 00000000..289f0827 --- /dev/null +++ b/docker-compose-combined.yml @@ -0,0 +1,249 @@ +name: styx + +services: + # ========================================= + # ZOOKEEPER QUORUM + # ========================================= + zoo1: + image: confluentinc/cp-zookeeper:7.4.0 + hostname: zoo1 + container_name: zoo1 + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_SERVER_ID: 1 + ZOOKEEPER_SERVERS: zoo1:2888:3888;zoo2:2888:3888;zoo3:2888:3888 + healthcheck: + test: ["CMD", "cub", "zk-ready", "localhost:2181", "10"] + interval: 10s + timeout: 5s + retries: 5 + + zoo2: + image: confluentinc/cp-zookeeper:7.4.0 + hostname: zoo2 + container_name: zoo2 + ports: + - "2182:2182" + environment: + ZOOKEEPER_CLIENT_PORT: 2182 + ZOOKEEPER_SERVER_ID: 2 + ZOOKEEPER_SERVERS: zoo1:2888:3888;zoo2:2888:3888;zoo3:2888:3888 + healthcheck: + test: ["CMD", "cub", "zk-ready", "localhost:2182", "10"] + interval: 10s + timeout: 5s + retries: 5 + + zoo3: + image: confluentinc/cp-zookeeper:7.4.0 + hostname: zoo3 + container_name: zoo3 + ports: + - "2183:2183" + environment: + ZOOKEEPER_CLIENT_PORT: 2183 + ZOOKEEPER_SERVER_ID: 3 + ZOOKEEPER_SERVERS: zoo1:2888:3888;zoo2:2888:3888;zoo3:2888:3888 + healthcheck: + test: ["CMD", "cub", "zk-ready", "localhost:2183", "10"] + interval: 10s + timeout: 5s + retries: 5 + + # ========================================= + # KAFKA CLUSTER + # ========================================= + kafka1: + image: confluentinc/cp-kafka:7.4.0 + hostname: kafka1 + container_name: kafka1 + ports: + - "9092:9092" + stop_grace_period: 10s + environment: + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181,zoo2:2182,zoo3:2183" + KAFKA_BROKER_ID: 1 + KAFKA_MESSAGE_MAX_BYTES: 134217728 + KAFKA_REPLICA_FETCH_MAX_BYTES: 134217728 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_MIN_INSYNC_REPLICAS: 1 + KAFKA_ZOOKEEPER_SESSION_TIMEOUT_MS: 6000 + KAFKA_ZOOKEEPER_CONNECTION_TIMEOUT_MS: 6000 + healthcheck: + test: ["CMD", "cub", "kafka-ready", "-b", "localhost:9092", "1", "20"] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + zoo1: + condition: service_healthy + zoo2: + condition: service_healthy + zoo3: + condition: service_healthy + + kafka2: + image: confluentinc/cp-kafka:7.4.0 + hostname: kafka2 + container_name: kafka2 + ports: + - "9093:9093" + stop_grace_period: 10s + environment: + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka2:19093,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9093 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181,zoo2:2182,zoo3:2183" + KAFKA_BROKER_ID: 2 + KAFKA_MESSAGE_MAX_BYTES: 134217728 + KAFKA_REPLICA_FETCH_MAX_BYTES: 134217728 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_MIN_INSYNC_REPLICAS: 1 + KAFKA_ZOOKEEPER_SESSION_TIMEOUT_MS: 6000 + KAFKA_ZOOKEEPER_CONNECTION_TIMEOUT_MS: 6000 + healthcheck: + test: ["CMD", "cub", "kafka-ready", "-b", "localhost:9093", "1", "20"] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + zoo1: + condition: service_healthy + zoo2: + condition: service_healthy + zoo3: + condition: service_healthy + + kafka3: + image: confluentinc/cp-kafka:7.4.0 + hostname: kafka3 + container_name: kafka3 + ports: + - "9094:9094" + stop_grace_period: 10s + environment: + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka3:19094,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9094 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181,zoo2:2182,zoo3:2183" + KAFKA_BROKER_ID: 3 + KAFKA_MESSAGE_MAX_BYTES: 134217728 # 128MB + KAFKA_REPLICA_FETCH_MAX_BYTES: 134217728 # 128MB + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 + KAFKA_MIN_INSYNC_REPLICAS: 1 + KAFKA_ZOOKEEPER_SESSION_TIMEOUT_MS: 6000 + KAFKA_ZOOKEEPER_CONNECTION_TIMEOUT_MS: 6000 + healthcheck: + test: ["CMD", "cub", "kafka-ready", "-b", "localhost:9094", "1", "20"] + interval: 10s + timeout: 5s + retries: 5 + depends_on: + zoo1: + condition: service_healthy + zoo2: + condition: service_healthy + zoo3: + condition: service_healthy + + # ========================================= + # STORAGE & OBSERVABILITY + # ========================================= + rustfs: + image: rustfs/rustfs:latest + ports: + - "9000:9000" + volumes: + - rustfs-data:/data + + prometheus: + image: prom/prometheus:v3.2.1 + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + command: + - "--config.file=/etc/prometheus/prometheus.yml" + + grafana: + image: grafana/grafana:11.5.2-ubuntu + ports: + - "3001:3000" + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/etc/grafana/dashboards + + # ========================================= + # STYX COMPONENTS + # ========================================= + coordinator: + build: + context: . + dockerfile: coordinator/coordinator.dockerfile + image: dev/styx-coordinator:latest + ports: + - "8886:8888" + - "8000:8000" + environment: + - KAFKA_URL=kafka1:19092 + - HEARTBEAT_LIMIT=5000 + - HEARTBEAT_CHECK_INTERVAL=500 + - S3_ENDPOINT=http://rustfs:9000 + - S3_ACCESS_KEY=rustfsadmin + - S3_SECRET_KEY=rustfsadmin + depends_on: + kafka1: + condition: service_healthy + kafka2: + condition: service_healthy + kafka3: + condition: service_healthy + rustfs: + condition: service_started + prometheus: + condition: service_started + grafana: + condition: service_started + + worker: + build: + context: . + dockerfile: worker/worker.dockerfile + image: dev/styx:latest + environment: + - INGRESS_TYPE=KAFKA + - KAFKA_URL=kafka1:19092 + - DISCOVERY_HOST=coordinator + - DISCOVERY_PORT=8888 + - S3_ENDPOINT=http://rustfs:9000 + - S3_ACCESS_KEY=rustfsadmin + - S3_SECRET_KEY=rustfsadmin + depends_on: + coordinator: + condition: service_started + kafka1: + condition: service_healthy + rustfs: + condition: service_started + deploy: + replicas: 3 + +volumes: + rustfs-data: + grafana_data: \ No newline at end of file diff --git a/docs/obol/index.md b/docs/obol/index.md new file mode 100644 index 00000000..9f7a09dd --- /dev/null +++ b/docs/obol/index.md @@ -0,0 +1,49 @@ +# Obol + +Obol is a source-to-source compiler that turns sequential, type-annotated, +object-oriented Python into the asynchronous, message-passing Styx operator +functions. You write entities and call their methods as if everything ran in a +single process, and Obol synthesizes the routing, state persistence, and +continuation management required by the Styx runtime — without weakening its +serializable, exactly-once transactional guarantees. + +```python +@entity +class User: + def __init__(self, name: str, balance: int): + self.name = name + self.balance = balance + self.myitems: list[Item] = [] + + def __key__(self) -> str: + return self.name + + def buy_item(self, amount: int, item: Item) -> bool: + total_price = amount * item.get_price() # cross-entity call, written as a normal call + if self.balance < total_price: + raise NotEnoughBalance("Not enough balance.") + item.update_stock(-amount) + self.balance -= total_price + self.myitems.append(item) + return True +``` + +Obol compiles this into a chain of registered Styx step functions, split at each +remote call, with live variables threaded across every asynchronous boundary via +an explicit `reply_to` continuation stack. + +The compiler lives under [`obol/`](https://github.com/delftdata/styx/tree/main/obol) +in the repository; see its `README.md` for the full programming model, the +compilation pipeline, limitations, and development instructions. + +## API Reference + +The Obol surface a program uses is the set of DSL intrinsics in `obol.api`: +`entity`, `send_async`, `gather`, `get_entity_by_key`, and `exists`. (The +compiler internals under `obol/src/obol` are not part of the public API.) + +::: obol.api + options: + show_submodules: false + show_root_heading: true + show_overloads: false diff --git a/mkdocs.yml b/mkdocs.yml index 794123ea..ff5dc849 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,7 @@ nav: - Docs: - Overview: styx-docs/overview.md - Quickstart: styx-docs/quickstart.md + - Obol: obol/index.md - Styx Operator: styx-docs/styx-operator.md - Stateful Function: styx-docs/styx-stateful-function.md - Stateflow Graph: styx-docs/styx-stateflow-graph.md @@ -78,7 +79,7 @@ plugins: - mkdocstrings: handlers: python: - paths: ["styx-package/styx"] + paths: ["styx-package/styx", "obol/src"] options: show_source: false merge_init_into_class: true diff --git a/obol/LICENSE b/obol/LICENSE new file mode 100644 index 00000000..b125e04d --- /dev/null +++ b/obol/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Alexandros Dimakos + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/obol/README.md b/obol/README.md new file mode 100644 index 00000000..e1ab33df --- /dev/null +++ b/obol/README.md @@ -0,0 +1,142 @@ +# Obol + +**Write distributed stateful workflows as ordinary sequential Python — let the compiler produce the distributed program.** + +Obol is a source-to-source compiler that takes type-annotated, object-oriented Python and compiles it into the asynchronous, message-passing operator functions required by the [Styx](https://github.com/delftdata/styx) stateful Function-as-a-Service (SFaaS) runtime. You write entities and call their methods as if everything ran in one process and Obol synthesizes the routing, state persistence, and continuation management. + +## The problem it solves + +On a distributed SFaaS runtime, each cross-entity call becomes a network message and the local stack frame is destroyed at every suspension point. A naturally sequential method must be hand-decomposed into a chain of callbacks, each with its own serialized context, return-address record, and explicit state-persistence call. Obol removes the need for this and instead distributed control flow is handled by the compiler. + +```python +# What you write (Obol) +@entity +class User: + def __init__(self, name: str, balance: int): + self.name = name + self.balance = balance + self.myitems: list[Item] = [] + + def __key__(self) -> str: + return self.name + + def buy_item(self, amount: int, item: Item) -> bool: + total_price = amount * item.get_price() # cross-entity call, written as a normal call + if self.balance < total_price: + raise NotEnoughBalance("Not enough balance.") + item.update_stock(-amount) + self.balance -= total_price + self.myitems.append(item) + return True +``` + +Obol compiles `buy_item` into a chain of registered Styx step functions, split at each remote call, with live variables threaded across every asynchronous boundary via an explicit `reply_to` continuation stack. + +## The programming model + +The surface is deliberately small: + +- **Entities** — a distributed stateful object is a class annotated with `@entity`. Its `__init__` attributes are its persistent state. +- **Keys** — each entity defines `__key__()` returning its routing key (a value, or a tuple for composite keys). Styx partitions instances by this key. +- **Methods** — plain synchronous Python over `self` and parameters. No `async def`, no `await`, no manual context dictionaries. +- **Calls** — a call on a value typed as an `@entity` class (e.g. `item.get_price()`) compiles to an asynchronous remote dispatch; everything else stays a local call. The receiver's *type* decides — there is no annotation to mark a call as remote. Use `get_entity_by_key(Item, key)` when you need a reference by key rather than as a parameter. + +Supported control flow includes `if`/`else`, `while`/`for` loops, `break`/`continue`, and recursion, with remote calls allowed anywhere inside them. + +### Concurrency + +By default, calls are sequential (each awaits the previous reply). Two constructs opt into parallelism: + +```python +from obol.api import send_async, gather + +# fire-and-forget: dispatch without awaiting; return value is suppressed +send_async(user.add_money(50)) + +# fan-out / fan-in: dispatch independent calls concurrently, bind results once all complete +price, discount = gather(item.get_price(), coupon.get_discount()) + +# dynamic fan-out over a comprehension +prices = gather(*[item.get_price() for item in self.myitems]) +``` + +`gather` mirrors `asyncio.gather` but compiles to a *persistent* synchronization barrier that survives worker failure. It either yields all results or the whole transaction aborts (no partial tuples, no `return_exceptions`). + +> `send_async`, `gather`, `get_entity_by_key`, and `exists` are **compiler intrinsics** — they are recognized and rewritten at compile time and raise if executed directly in plain Python. The `examples/original/` programs are compiler *inputs*, not scripts to run. + +## How it works + +Obol is a multi-stage pipeline over the [libcst](https://github.com/Instagram/LibCST) concrete syntax tree: + +1. **Syntactic preparation** — expand comprehensions containing remote calls into explicit loops, guard short-circuit (`and`/`or`) operands, and linearize so every remote call is a standalone top-level assignment (partial A-Normal Form). +2. **Type resolution** — use `mypy` metadata to resolve each call's receiver type and identify which call sites target `@entity` classes. Unresolvable call sites are rejected. +3. **Live-variable analysis** — a backward dataflow analysis (via [`libcst-dfa`](https://pypi.org/project/libcst-dfa/)) computes the minimal set of variables to serialize across each asynchronous boundary. +4. **Function splitting** — partition each method at its remote-call boundaries into a chain of step functions (CPS + defunctionalization). Loops become tail-recursive step functions; recursion reuses the `reply_to` stack as a distributed call stack. + +Every cross-entity call in the source compiles to exactly one asynchronous dispatch in the output — no extra round-trips are introduced. + +## Limitations + +Obol compiles a typed subset of Python, not the whole language. + +**Static typing.** All entity-typed values must carry **type annotations**; a program that doesn't fully type-check under `mypy`, or has a call site whose receiver type can't be resolved, is rejected. The full message-passing structure of the output must be determined statically. + +**No aliasing of mutable entity state across a remote call.** Local variables are serialized into the continuation context at each split point, while entity state (`self`) is re-read fresh at the start of every step. A local bound to a *mutable* state field therefore becomes a stale snapshot the moment a remote call splits the method: + +```python +items = self.myitems # local alias of a mutable state field +total = other.compute() # remote call → split; `items` is now frozen +items.append(x) # mutates the snapshot, not the live state — lost on resume +``` + +Re-read `self.myitems` after the call, or take an explicit copy when you only need a read-only view. Immutable values and entity references (which travel as keys, not objects) are safe to bind to locals. + +**No inheritance.** Entities are flat. The compiler reads each entity's state from its own `__init__` and compiles only the methods defined directly on the `@entity` class — base classes are ignored. An `@entity` class cannot subclass another entity or inherit state/methods from a shared base. + +**Entity shape.** Every `@entity` class must define `__init__` (which declares its persistent state) and `__key__` returning `self.` or a tuple of `self.`s; key components must be `__init__` parameters. Composite keys are concatenated into a single string key. + +**Other constructs.** +- **Generator expressions** containing remote calls are rejected (eager materialization would change their lazy semantics) — use a list comprehension or an explicit loop. +- `exists()` is only supported as `exists(self)`. +- Fire-and-forget (`send_async`) suppresses the callee's return value by design, and `gather` provides no `return_exceptions` equivalent — a fanned-out failure aborts the whole transaction. + +--- + +## Development + +Obol is a self-contained [uv](https://docs.astral.sh/uv/) project. Run all commands from this directory (`obol/`). + +### Install + +```bash +uv sync # runtime + dev dependencies +uv sync --no-dev # runtime only +uv run prek install # optional: install the pre-commit hooks +``` + +### Compile a program + +```bash +uv run obol [output.py] +``` + +- `input` — path to the program to compile. +- `output` — optional; where to write the compiled code. Defaults to `examples/compiled/`. + +```bash +uv run obol # default: examples/original/user_item.py -> examples/compiled/user_item.py +uv run obol path/to/program.py # -> examples/compiled/program.py +uv run obol path/to/program.py out/result.py # explicit input and output paths +``` + +### Test, lint, docs + +```bash +uv run pytest --cov=src ./tests # tests +uv run prek run --all-files # ruff + yamlfix + checks +uv run mkdocs serve --watch ./ # docs locally +``` + +## License + +Distributed under the terms of the [Apache 2.0 License](LICENSE). diff --git a/obol/examples/benchmarks/compiled/tpcc.py b/obol/examples/benchmarks/compiled/tpcc.py new file mode 100644 index 00000000..33bfcbe1 --- /dev/null +++ b/obol/examples/benchmarks/compiled/tpcc.py @@ -0,0 +1,771 @@ +import uuid +from styx.common.operator import Operator +from styx.common.stateful_function import StatefulFunction +from styx.common.logging import logging + +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None + +from __future__ import annotations +from typing import Any, Dict, Optional +import datetime + + +# ────────────────────────────────────────── +# Exceptions +# ────────────────────────────────────────── + +class InsufficientStock(Exception): + pass + +class InvalidItem(Exception): + pass + +class WHDoesNotExist(Exception): + pass + +class DistrictDoesNotExist(Exception): + pass + +class TPCCException(Exception): + pass + +class CustomerDoesNotExist(Exception): + pass + +class HistoryDoesNotExist(Exception): + pass + +class StockDoesNotExist(Exception): + pass + +class OrderDoesNotExist(Exception): + pass + +class OrderLineDoesNotExist(Exception): + pass +warehouse_operator = Operator('warehouse', n_partitions=4) + +@warehouse_operator.register +async def insert(ctx: StatefulFunction, w_id: int, W_NAME: str, W_STREET_1: str, W_STREET_2: str, + W_CITY: str, W_STATE: str, W_ZIP: str, W_TAX: float, W_YTD: float, reply_to: list = None): + __state__ = {} + __state__['w_id'] = w_id + __state__['W_NAME'] = W_NAME + __state__['W_STREET_1'] = W_STREET_1 + __state__['W_STREET_2'] = W_STREET_2 + __state__['W_CITY'] = W_CITY + __state__['W_STATE'] = W_STATE + __state__['W_ZIP'] = W_ZIP + __state__['W_TAX'] = W_TAX + __state__['W_YTD'] = W_YTD + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@warehouse_operator.register +async def get_warehouse(ctx: StatefulFunction, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise WHDoesNotExist(f"Warehouse with key: {ctx.key} does not exist.") + data = { + 'W_NAME': __state__['W_NAME'], 'W_TAX': __state__['W_TAX'], 'W_YTD': __state__['W_YTD'], + 'W_STREET_1': __state__['W_STREET_1'], 'W_STREET_2': __state__['W_STREET_2'], + 'W_CITY': __state__['W_CITY'], 'W_STATE': __state__['W_STATE'], 'W_ZIP': __state__['W_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@warehouse_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise WHDoesNotExist(f"Warehouse with key: {ctx.key} does not exist") + __state__['W_YTD'] = float(__state__['W_YTD']) + h_amount + data = { + 'W_NAME': __state__['W_NAME'], 'W_TAX': __state__['W_TAX'], 'W_YTD': __state__['W_YTD'], + 'W_STREET_1': __state__['W_STREET_1'], 'W_STREET_2': __state__['W_STREET_2'], + 'W_CITY': __state__['W_CITY'], 'W_STATE': __state__['W_STATE'], 'W_ZIP': __state__['W_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +district_operator = Operator('district', n_partitions=4, composite_key_hash_params=(0, ':')) + +@district_operator.register +async def insert(ctx: StatefulFunction, D_ID: int, D_W_ID: int, D_NAME: str, D_STREET_1: str, D_STREET_2: str, + D_CITY: str, D_STATE: str, D_ZIP: str, D_TAX: float, D_YTD: float, + D_NEXT_O_ID: int, reply_to: list = None): + __state__ = {} + __state__['D_ID'] = D_ID + __state__['D_W_ID'] = D_W_ID + __state__['D_NAME'] = D_NAME + __state__['D_STREET_1'] = D_STREET_1 + __state__['D_STREET_2'] = D_STREET_2 + __state__['D_CITY'] = D_CITY + __state__['D_STATE'] = D_STATE + __state__['D_ZIP'] = D_ZIP + __state__['D_TAX'] = D_TAX + __state__['D_YTD'] = D_YTD + __state__['D_NEXT_O_ID'] = D_NEXT_O_ID + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@district_operator.register +async def get_district(ctx: StatefulFunction, w_id: int, d_id: int, c_id: int, + o_entry_d: str, i_ids: list[int], i_qtys: list[int], + i_w_ids: list[int], all_local: bool, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise DistrictDoesNotExist(f"District with key: {ctx.key} does not exist") + d_next_o_id = __state__['D_NEXT_O_ID'] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'order', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(d_next_o_id), params = (w_id, d_id, d_next_o_id, c_id, o_entry_d, None, len(i_ids), all_local, [{'sink': True}])) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'neworder', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(d_next_o_id), params = (w_id, d_id, d_next_o_id, [{'sink': True}])) + _g_iter = list(range(len(i_ids))) + _gather_id = init_gather_barrier(ctx, len(_g_iter), {}, reply_to) + for (_g_tag, i) in enumerate(_g_iter): + _g_reply = [{'op_name': 'district', 'fun': 'get_district_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': _g_tag}}] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_item', key = i_ids[i], params = (i, w_id, d_id, o_entry_d, i_qtys[i], i_w_ids[i], d_next_o_id, _g_reply)) + ctx.put(__state__) + +@district_operator.register +async def get_district_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + __state__ = ctx.get() or {} + reply_to = parent_reply_to + item_replies = _g_results + __state__['D_NEXT_O_ID'] += 1 + data = { + 'D_ID': __state__['D_ID'], 'D_W_ID': __state__['D_W_ID'], 'D_NAME': __state__['D_NAME'], + 'D_TAX': __state__['D_TAX'], 'D_YTD': __state__['D_YTD'], 'D_NEXT_O_ID': __state__['D_NEXT_O_ID'], + 'D_STREET_1': __state__['D_STREET_1'], 'D_STREET_2': __state__['D_STREET_2'], + 'D_CITY': __state__['D_CITY'], 'D_STATE': __state__['D_STATE'], 'D_ZIP': __state__['D_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, {'district': data, 'items': item_replies}) + + +@district_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise DistrictDoesNotExist(f"District with key: {ctx.key} does not exist") + __state__['D_YTD'] = float(__state__['D_YTD']) + h_amount + data = { + 'D_ID': __state__['D_ID'], 'D_W_ID': __state__['D_W_ID'], 'D_NAME': __state__['D_NAME'], + 'D_TAX': __state__['D_TAX'], 'D_YTD': __state__['D_YTD'], 'D_NEXT_O_ID': __state__['D_NEXT_O_ID'], + 'D_STREET_1': __state__['D_STREET_1'], 'D_STREET_2': __state__['D_STREET_2'], + 'D_CITY': __state__['D_CITY'], 'D_STATE': __state__['D_STATE'], 'D_ZIP': __state__['D_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +item_operator = Operator('item', n_partitions=4) + +@item_operator.register +async def insert(ctx: StatefulFunction, I_ID: int, I_IM_ID: int, I_NAME: str, I_PRICE: float, I_DATA: str, reply_to: list = None): + __state__ = {} + __state__['I_ID'] = I_ID + __state__['I_IM_ID'] = I_IM_ID + __state__['I_NAME'] = I_NAME + __state__['I_PRICE'] = I_PRICE + __state__['I_DATA'] = I_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@item_operator.register +async def get_item(ctx: StatefulFunction, index: int, w_id: int, d_id: int, + o_entry_d: str, i_qty: int, i_w_id: int, d_next_o_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + + if not bool(__state__): + raise TPCCException("Item number is not valid") + attr_1 = __state__['I_DATA'].find("original") + i_brand_generic = attr_1 != -1 + stock = f"{i_w_id}:{ctx.key}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'stock', function_name = 'update_stock', key = stock, params = (index, d_next_o_id, ctx.key, w_id, d_id, i_w_id, o_entry_d, i_qty, __state__['I_NAME'], __state__['I_PRICE'], i_brand_generic, reply_to)) + +customer_operator = Operator('customer', n_partitions=4, composite_key_hash_params=(0, ':')) + +@customer_operator.register +async def insert(ctx: StatefulFunction, C_ID: int, C_D_ID: int, C_W_ID: int, C_FIRST: str, C_MIDDLE: str, + C_LAST: str, C_STREET_1: str, C_STREET_2: str, C_CITY: str, C_STATE: str, + C_ZIP: str, C_PHONE: str, C_SINCE: str, C_CREDIT: str, + C_CREDIT_LIM: float, C_DISCOUNT: float, C_BALANCE: float, + C_YTD_PAYMENT: float, C_PAYMENT_CNT: int, C_DELIVERY_CNT: int, C_DATA: str, reply_to: list = None): + __state__ = {} + __state__['C_ID'] = C_ID + __state__['C_D_ID'] = C_D_ID + __state__['C_W_ID'] = C_W_ID + __state__['C_FIRST'] = C_FIRST + __state__['C_MIDDLE'] = C_MIDDLE + __state__['C_LAST'] = C_LAST + __state__['C_STREET_1'] = C_STREET_1 + __state__['C_STREET_2'] = C_STREET_2 + __state__['C_CITY'] = C_CITY + __state__['C_STATE'] = C_STATE + __state__['C_ZIP'] = C_ZIP + __state__['C_PHONE'] = C_PHONE + __state__['C_SINCE'] = C_SINCE + __state__['C_CREDIT'] = C_CREDIT + __state__['C_CREDIT_LIM'] = C_CREDIT_LIM + __state__['C_DISCOUNT'] = C_DISCOUNT + __state__['C_BALANCE'] = C_BALANCE + __state__['C_YTD_PAYMENT'] = C_YTD_PAYMENT + __state__['C_PAYMENT_CNT'] = C_PAYMENT_CNT + __state__['C_DELIVERY_CNT'] = C_DELIVERY_CNT + __state__['C_DATA'] = C_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@customer_operator.register +async def get_customer(ctx: StatefulFunction, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise CustomerDoesNotExist(f"Customer with id: {ctx.key} does not exist") + data = { + 'C_ID': __state__['C_ID'], 'C_D_ID': __state__['C_D_ID'], 'C_W_ID': __state__['C_W_ID'], + 'C_FIRST': __state__['C_FIRST'], 'C_MIDDLE': __state__['C_MIDDLE'], 'C_LAST': __state__['C_LAST'], + 'C_STREET_1': __state__['C_STREET_1'], 'C_STREET_2': __state__['C_STREET_2'], + 'C_CITY': __state__['C_CITY'], 'C_STATE': __state__['C_STATE'], 'C_ZIP': __state__['C_ZIP'], + 'C_PHONE': __state__['C_PHONE'], 'C_SINCE': __state__['C_SINCE'], 'C_CREDIT': __state__['C_CREDIT'], + 'C_CREDIT_LIM': __state__['C_CREDIT_LIM'], 'C_DISCOUNT': __state__['C_DISCOUNT'], + 'C_BALANCE': __state__['C_BALANCE'], 'C_YTD_PAYMENT': __state__['C_YTD_PAYMENT'], + 'C_PAYMENT_CNT': __state__['C_PAYMENT_CNT'], 'C_DELIVERY_CNT': __state__['C_DELIVERY_CNT'], + 'C_DATA': __state__['C_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@customer_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, d_id: int, w_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise CustomerDoesNotExist(f"Customer with id: {ctx.key} does not exist") + __state__['C_BALANCE'] = float(__state__['C_BALANCE']) - h_amount + __state__['C_YTD_PAYMENT'] = float(__state__['C_YTD_PAYMENT']) + h_amount + __state__['C_PAYMENT_CNT'] = int(__state__['C_PAYMENT_CNT']) + 1 + + if __state__['C_CREDIT'] == "BC": + new_data = f"{__state__['C_ID']} {__state__['C_D_ID']} {__state__['C_W_ID']} {d_id} {w_id} {h_amount}" + __state__['C_DATA'] = (new_data + "|" + __state__['C_DATA']) + + if len(__state__['C_DATA']) > 500: + __state__['C_DATA'] = __state__['C_DATA'][:500] + data = { + 'C_ID': __state__['C_ID'], 'C_D_ID': __state__['C_D_ID'], 'C_W_ID': __state__['C_W_ID'], + 'C_FIRST': __state__['C_FIRST'], 'C_MIDDLE': __state__['C_MIDDLE'], 'C_LAST': __state__['C_LAST'], + 'C_STREET_1': __state__['C_STREET_1'], 'C_STREET_2': __state__['C_STREET_2'], + 'C_CITY': __state__['C_CITY'], 'C_STATE': __state__['C_STATE'], 'C_ZIP': __state__['C_ZIP'], + 'C_PHONE': __state__['C_PHONE'], 'C_SINCE': __state__['C_SINCE'], 'C_CREDIT': __state__['C_CREDIT'], + 'C_CREDIT_LIM': __state__['C_CREDIT_LIM'], 'C_DISCOUNT': __state__['C_DISCOUNT'], + 'C_BALANCE': __state__['C_BALANCE'], 'C_YTD_PAYMENT': __state__['C_YTD_PAYMENT'], + 'C_PAYMENT_CNT': __state__['C_PAYMENT_CNT'], 'C_DELIVERY_CNT': __state__['C_DELIVERY_CNT'], + 'C_DATA': __state__['C_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +customerindex_operator = Operator('customerindex', n_partitions=4, composite_key_hash_params=(0, ':')) + +@customerindex_operator.register +async def insert(ctx: StatefulFunction, C_W_ID: int, C_D_ID: int, C_LAST: str, customers: list[str], reply_to: list = None): + __state__ = {} + __state__['C_W_ID'] = C_W_ID + __state__['C_D_ID'] = C_D_ID + __state__['C_LAST'] = C_LAST + __state__['customers'] = customers + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@customerindex_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, d_id: int, w_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + index = (len(__state__['customers']) - 1) // 2 + customer = __state__['customers'][index] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'pay', key = customer, params = (h_amount, d_id, w_id, reply_to)) + +stock_operator = Operator('stock', n_partitions=4, composite_key_hash_params=(0, ':')) + +@stock_operator.register +async def insert(ctx: StatefulFunction, S_I_ID: int, S_W_ID: int, S_QUANTITY: int, + S_DIST_01: str, S_DIST_02: str, S_DIST_03: str, S_DIST_04: str, + S_DIST_05: str, S_DIST_06: str, S_DIST_07: str, S_DIST_08: str, + S_DIST_09: str, S_DIST_10: str, S_YTD: int, S_ORDER_CNT: int, + S_REMOTE_CNT: int, S_DATA: str, reply_to: list = None): + __state__ = {} + __state__['S_I_ID'] = S_I_ID + __state__['S_W_ID'] = S_W_ID + __state__['S_QUANTITY'] = S_QUANTITY + __state__['S_DIST_01'] = S_DIST_01 + __state__['S_DIST_02'] = S_DIST_02 + __state__['S_DIST_03'] = S_DIST_03 + __state__['S_DIST_04'] = S_DIST_04 + __state__['S_DIST_05'] = S_DIST_05 + __state__['S_DIST_06'] = S_DIST_06 + __state__['S_DIST_07'] = S_DIST_07 + __state__['S_DIST_08'] = S_DIST_08 + __state__['S_DIST_09'] = S_DIST_09 + __state__['S_DIST_10'] = S_DIST_10 + __state__['S_YTD'] = S_YTD + __state__['S_ORDER_CNT'] = S_ORDER_CNT + __state__['S_REMOTE_CNT'] = S_REMOTE_CNT + __state__['S_DATA'] = S_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@stock_operator.register +async def get_stock(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + data = { + 'S_I_ID': __state__['S_I_ID'].I_ID, 'S_W_ID': __state__['S_W_ID'], 'S_QUANTITY': __state__['S_QUANTITY'], + 'S_DIST_01': __state__['S_DIST_01'], 'S_DIST_02': __state__['S_DIST_02'], 'S_DIST_03': __state__['S_DIST_03'], + 'S_DIST_04': __state__['S_DIST_04'], 'S_DIST_05': __state__['S_DIST_05'], 'S_DIST_06': __state__['S_DIST_06'], + 'S_DIST_07': __state__['S_DIST_07'], 'S_DIST_08': __state__['S_DIST_08'], 'S_DIST_09': __state__['S_DIST_09'], + 'S_DIST_10': __state__['S_DIST_10'], 'S_YTD': __state__['S_YTD'], 'S_ORDER_CNT': __state__['S_ORDER_CNT'], + 'S_REMOTE_CNT': __state__['S_REMOTE_CNT'], 'S_DATA': __state__['S_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@stock_operator.register +async def update_stock(ctx: StatefulFunction, index: int, o_id: int, i_id: int, + w_id: int, d_id: int, i_w_id: int, o_entry_d: str, i_qty: int, + i_name: str, i_price: float, i_brand_generic: bool, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + + if not bool(__state__): + raise StockDoesNotExist(f"Stock with key: {ctx.key} does not exist") + __state__['S_YTD'] += i_qty + if __state__['S_QUANTITY'] >= i_qty + 10: + __state__['S_QUANTITY'] -= i_qty + else: + __state__['S_QUANTITY'] = __state__['S_QUANTITY'] + 91 - i_qty + __state__['S_ORDER_CNT'] += 1 + + if i_w_id != w_id: + __state__['S_REMOTE_CNT'] += 1 + + if i_brand_generic: + if "original" in __state__['S_DATA']: + brand_generic = "B" + else: + brand_generic = "G" + else: + brand_generic = "G" + ol_amount = i_qty * i_price + dist = ( + __state__['S_DIST_01'], + __state__['S_DIST_02'], + __state__['S_DIST_03'], + __state__['S_DIST_04'], + __state__['S_DIST_05'], + __state__['S_DIST_06'], + __state__['S_DIST_07'], + __state__['S_DIST_08'], + __state__['S_DIST_09'], + __state__['S_DIST_10'], + ) + s_dist_xx = dist[d_id - 1] + ol_number = index + 1 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'orderline', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(o_id) + ":" + str(ol_number), params = (w_id, d_id, o_id, i_id, ol_number, i_qty, o_entry_d, i_w_id, s_dist_xx, ol_amount, [{'sink': True}])) + ctx.put(__state__) + return send_reply(ctx, reply_to, { + 'i_name': i_name, + 'i_price': i_price, + 'ol_amount': ol_amount, + 's_quantity': __state__['S_QUANTITY'], + 'brand_generic': brand_generic, + }) + +history_operator = Operator('history', n_partitions=4, composite_key_hash_params=(0, ':')) + +@history_operator.register +async def insert(ctx: StatefulFunction, H_C_ID: int, H_C_D_ID: int, H_C_W_ID: int, + H_D_ID: int, H_W_ID: int, H_DATE: str, H_AMOUNT: float, H_DATA: str, reply_to: list = None): + __state__ = {} + __state__['H_C_ID'] = H_C_ID + __state__['H_C_D_ID'] = H_C_D_ID + __state__['H_C_W_ID'] = H_C_W_ID + __state__['H_D_ID'] = H_D_ID + __state__['H_W_ID'] = H_W_ID + __state__['H_DATE'] = H_DATE + __state__['H_AMOUNT'] = H_AMOUNT + __state__['H_DATA'] = H_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@history_operator.register +async def get_history(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise HistoryDoesNotExist(f"History with key: {ctx.key} does not exist") + data = { + 'H_C_ID': __state__['H_C_ID'], 'H_C_D_ID': __state__['H_C_D_ID'], 'H_C_W_ID': __state__['H_C_W_ID'], + 'H_D_ID': __state__['H_D_ID'], 'H_W_ID': __state__['H_W_ID'], 'H_DATE': __state__['H_DATE'], + 'H_AMOUNT': __state__['H_AMOUNT'], 'H_DATA': __state__['H_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +order_operator = Operator('order', n_partitions=4, composite_key_hash_params=(0, ':')) + +@order_operator.register +async def insert(ctx: StatefulFunction, O_W_ID: int, O_D_ID: int, O_ID: int, O_C_ID: int = 0, O_ENTRY_D: str = "", O_CARRIER_ID: Optional[int] = None, O_OL_CNT: int = 0, O_ALL_LOCAL: bool = True, reply_to: list = None): + __state__ = {} + __state__['O_W_ID'] = O_W_ID + __state__['O_D_ID'] = O_D_ID + __state__['O_ID'] = O_ID + __state__['O_C_ID'] = O_C_ID + __state__['O_ENTRY_D'] = O_ENTRY_D + __state__['O_CARRIER_ID'] = O_CARRIER_ID + __state__['O_OL_CNT'] = O_OL_CNT + __state__['O_ALL_LOCAL'] = O_ALL_LOCAL + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@order_operator.register +async def get_order(ctx: StatefulFunction, c_id: int, entry_d: str, ol_cnt: int, all_local: bool, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + data = { + 'O_W_ID': __state__['O_W_ID'], 'O_D_ID': __state__['O_D_ID'], 'O_ID': __state__['O_ID'], 'O_C_ID': c_id, 'O_ENTRY_D': entry_d, + 'O_OL_CNT': ol_cnt, 'O_ALL_LOCAL': all_local, + } + if not bool(__state__): + raise OrderDoesNotExist(f"Order with key: {ctx.key} does not exist") + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +neworder_operator = Operator('neworder', n_partitions=4, composite_key_hash_params=(0, ':')) + +@neworder_operator.register +async def insert(ctx: StatefulFunction, NO_W_ID: int, NO_D_ID: int, NO_O_ID: int, reply_to: list = None): + __state__ = {} + __state__['NO_W_ID'] = NO_W_ID + __state__['NO_D_ID'] = NO_D_ID + __state__['NO_O_ID'] = NO_O_ID + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@neworder_operator.register +async def create(ctx: StatefulFunction, no_o_id: int, no_d_id: int, no_w_id: int, reply_to: list = None) -> None: + __state__ = ctx.get() or {} + __state__['NO_O_ID'] = no_o_id + __state__['NO_D_ID'] = no_d_id + __state__['NO_W_ID'] = no_w_id + ctx.put(__state__) + +orderline_operator = Operator('orderline', n_partitions=4, composite_key_hash_params=(0, ':')) + +@orderline_operator.register +async def insert( + ctx: StatefulFunction, OL_W_ID: int, + OL_D_ID: int, + OL_O_ID: int, + OL_I_ID: int, + OL_NUMBER: int, + OL_QUANTITY: int = 0, + OL_DELIVERY_D: Optional[str] = None, + OL_SUPPLY_W_ID: Optional[int] = None, + OL_DIST_INFO: str = "", + OL_AMOUNT: float = 0.0, +reply_to: list = None): + __state__ = {} + __state__['OL_W_ID'] = OL_W_ID + __state__['OL_D_ID'] = OL_D_ID + __state__['OL_O_ID'] = OL_O_ID + __state__['OL_I_ID'] = OL_I_ID + __state__['OL_NUMBER'] = OL_NUMBER + __state__['OL_QUANTITY'] = OL_QUANTITY + __state__['OL_DELIVERY_D'] = OL_DELIVERY_D + __state__['OL_SUPPLY_W_ID'] = OL_SUPPLY_W_ID + __state__['OL_DIST_INFO'] = OL_DIST_INFO + __state__['OL_AMOUNT'] = OL_AMOUNT + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@orderline_operator.register +async def get_order_line(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise OrderLineDoesNotExist(f"OrderLine with key: {ctx.key} does not exist") + data = { + 'OL_W_ID': __state__['OL_W_ID'], 'OL_D_ID': __state__['OL_D_ID'], 'OL_O_ID': __state__['OL_O_ID'], + 'OL_I_ID': __state__['OL_I_ID'].I_ID, 'OL_NUMBER': __state__['OL_NUMBER'], 'OL_QUANTITY': __state__['OL_QUANTITY'], + 'OL_DELIVERY_D': __state__['OL_DELIVERY_D'], 'OL_SUPPLY_W_ID': __state__['OL_SUPPLY_W_ID'] if __state__['OL_SUPPLY_W_ID'] else None, + 'OL_DIST_INFO': __state__['OL_DIST_INFO'], 'OL_AMOUNT': __state__['OL_AMOUNT'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +newordertxn_operator = Operator('newordertxn', n_partitions=4) + +@newordertxn_operator.register +async def insert(ctx: StatefulFunction, txn_id: str, reply_to: list = None): + __state__ = {} + __state__['txn_id'] = txn_id + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@newordertxn_operator.register +async def new_order(ctx: StatefulFunction, params: dict, reply_to: list = None) -> str: + w_id: int = params["W_ID"] + d_id: int = params["D_ID"] + c_id: int = params["C_ID"] + o_entry_d: str = params["O_ENTRY_D"] + i_ids: list[int] = params["I_IDS"] + i_w_ids: list[int] = params["I_W_IDS"] + i_qtys: list[int] = params["I_QTYS"] + assert len(i_ids) > 0 + assert len(i_ids) == len(i_w_ids) == len(i_qtys) + all_local = True + for item_w_id in i_w_ids: + if item_w_id != w_id: + all_local = False + break + district = f"{w_id}:{d_id}" + customer = f"{w_id}:{d_id}:{c_id}" + _gather_id = init_gather_barrier(ctx, 3, {'o_entry_d': o_entry_d}, reply_to) + _g_reply_0 = [{'op_name': 'newordertxn', 'fun': 'new_order_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 0}}] + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_warehouse', key = w_id, params = (_g_reply_0,)) + _g_reply_1 = [{'op_name': 'newordertxn', 'fun': 'new_order_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 1}}] + ctx.call_remote_async(operator_name = 'district', function_name = 'get_district', key = district, params = (w_id, d_id, c_id, o_entry_d, i_ids, i_qtys, i_w_ids, all_local, _g_reply_1)) + _g_reply_2 = [{'op_name': 'newordertxn', 'fun': 'new_order_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 2}}] + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_customer', key = customer, params = (_g_reply_2,)) + +@newordertxn_operator.register +async def new_order_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + (o_entry_d,) = (saved.get('o_entry_d'),) + reply_to = parent_reply_to + warehouse_data, district_bundle, customer_data = _g_results + district_data = district_bundle['district'] + item_replies = district_bundle['items'] + total = sum(item_reply['ol_amount'] for item_reply in item_replies) + w_tax: float = warehouse_data['W_TAX'] + d_tax: float = district_data['D_TAX'] + total = total * (1 - customer_data['C_DISCOUNT']) * (1 + w_tax + d_tax) + o_id = district_data['D_NEXT_O_ID'] + attr_1 = ";" + item_str = attr_1.join( + f"{r['i_name']},{r['s_quantity']},{r['brand_generic']},{r['i_price']:.2f},{r['ol_amount']:.2f}" + for r in item_replies + ) + return send_reply(ctx, reply_to, ( + f"NO|C_ID={customer_data['C_ID']},C_LAST={customer_data['C_LAST']}," + f"C_CREDIT={customer_data['C_CREDIT']}," + f"C_DISCOUNT={customer_data['C_DISCOUNT']:.4f},W_TAX={w_tax:.4f},D_TAX={d_tax:.4f}," + f"O_ID={o_id},O_ENTRY_D={o_entry_d},N_ITEMS={len(item_replies)}," + f"TOTAL={total:.2f},ITEMS=[{item_str}]" + )) + +paymenttxn_operator = Operator('paymenttxn', n_partitions=4) + +@paymenttxn_operator.register +async def insert( + ctx: StatefulFunction, txn_id: str, + w_id: int, + c_w_id: int, + d_id: int = 0, + c_d_id: int = 0, + h_amount: float = 0.0, + h_date: str = "", +reply_to: list = None): + __state__ = {} + __state__['txn_id'] = txn_id + __state__['W_ID'] = w_id + __state__['D_ID'] = d_id + __state__['C_W_ID'] = c_w_id + __state__['C_D_ID'] = c_d_id + __state__['C_ID'] = None + __state__['H_AMOUNT'] = h_amount + __state__['H_DATE'] = h_date + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + + +@paymenttxn_operator.register +async def get_customer_data(ctx: StatefulFunction, c_last: Optional[str], reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if __state__['C_ID'] is not None: + customer = f"{__state__['C_W_ID']}:{__state__['C_D_ID']}:{__state__['C_ID']}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'pay', key = customer, params = (__state__['H_AMOUNT'], __state__['D_ID'], __state__['W_ID'], reply_to)) + else: + customer_idx = f"{__state__['C_W_ID']}:{__state__['C_D_ID']}:{c_last}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customerindex', function_name = 'pay', key = customer_idx, params = (__state__['H_AMOUNT'], __state__['D_ID'], __state__['W_ID'], reply_to)) + + +@paymenttxn_operator.register +async def payment(ctx: StatefulFunction, params: dict, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + w_id: int = params["W_ID"] + d_id: int = int(params["D_ID"]) + h_amount: float = params["H_AMOUNT"] + c_w_id: int = params["C_W_ID"] + c_d_id: int = int(params["C_D_ID"]) + attr_1 = params.get("C_ID") + c_id: Optional[int] = int(params["C_ID"]) if attr_1 is not None else None + attr_2 = params.get("C_LAST") + c_last: Optional[str] = attr_2 + h_date: str = params["H_DATE"] + __state__['W_ID'] = w_id + __state__['D_ID'] = d_id + __state__['C_ID'] = c_id + __state__['C_W_ID'] = c_w_id + __state__['C_D_ID'] = c_d_id + __state__['H_DATE'] = h_date + __state__['H_AMOUNT'] = h_amount + district = f"{w_id}:{d_id}" + _gather_id = init_gather_barrier(ctx, 3, {}, reply_to) + _g_reply_0 = [{'op_name': 'paymenttxn', 'fun': 'payment_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 0}}] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'paymenttxn', function_name = 'get_customer_data', key = ctx.key, params = (c_last, _g_reply_0)) + _g_reply_1 = [{'op_name': 'paymenttxn', 'fun': 'payment_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 1}}] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'district', function_name = 'pay', key = district, params = (h_amount, _g_reply_1)) + _g_reply_2 = [{'op_name': 'paymenttxn', 'fun': 'payment_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 2}}] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'pay', key = w_id, params = (h_amount, _g_reply_2)) + +@paymenttxn_operator.register +async def payment_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + __state__ = ctx.get() or {} + reply_to = parent_reply_to + customer_data, district_data, warehouse_data = _g_results + h_data = f"{warehouse_data['W_NAME']} {district_data['D_NAME']}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'history', function_name = 'insert', key = str(__state__['W_ID']) + ":" + str(__state__['D_ID']) + ":" + str(customer_data['C_ID']), params = (customer_data['C_ID'], __state__['C_D_ID'], __state__['C_W_ID'], __state__['D_ID'], __state__['W_ID'], __state__['H_DATE'], __state__['H_AMOUNT'], h_data, [{'sink': True}])) + + if customer_data['C_CREDIT'] == "BC": + c_data_str = f",C_DATA={customer_data['C_DATA'][:200]}" + else: + c_data_str = "" + ctx.put(__state__) + return send_reply(ctx, reply_to, ( + f"P|W_ID={__state__['W_ID']},D_ID={district_data['D_ID']},C_ID={customer_data['C_ID']}," + f"C_D_ID={customer_data['C_D_ID']},C_W_ID={customer_data['C_W_ID']}," + f"C_NAME={customer_data['C_FIRST']} {customer_data['C_MIDDLE']} {customer_data['C_LAST']}," + f"C_BAL={customer_data['C_BALANCE']:.2f},C_DISCOUNT={customer_data['C_DISCOUNT']:.4f}," + f"C_CREDIT={customer_data['C_CREDIT']},W_TAX={warehouse_data['W_TAX']:.4f}," + f"D_TAX={district_data['D_TAX']:.4f},H_AMOUNT={__state__['H_AMOUNT']:.2f}," + f"H_DATE={__state__['H_DATE']}{c_data_str}" + )) + diff --git a/obol/examples/benchmarks/compiled/tpcc_no_gather.py b/obol/examples/benchmarks/compiled/tpcc_no_gather.py new file mode 100644 index 00000000..10348aa9 --- /dev/null +++ b/obol/examples/benchmarks/compiled/tpcc_no_gather.py @@ -0,0 +1,789 @@ +import uuid +from styx.common.operator import Operator +from styx.common.stateful_function import StatefulFunction +from styx.common.logging import logging + +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None + +from __future__ import annotations +from typing import Any, Dict, Optional +import datetime + + +# ────────────────────────────────────────── +# Exceptions +# ────────────────────────────────────────── + +class InsufficientStock(Exception): + pass + +class InvalidItem(Exception): + pass + +class WHDoesNotExist(Exception): + pass + +class DistrictDoesNotExist(Exception): + pass + +class TPCCException(Exception): + pass + +class CustomerDoesNotExist(Exception): + pass + +class HistoryDoesNotExist(Exception): + pass + +class StockDoesNotExist(Exception): + pass + +class OrderDoesNotExist(Exception): + pass + +class OrderLineDoesNotExist(Exception): + pass +warehouse_operator = Operator('warehouse', n_partitions=4) + +@warehouse_operator.register +async def insert(ctx: StatefulFunction, w_id: int, W_NAME: str, W_STREET_1: str, W_STREET_2: str, + W_CITY: str, W_STATE: str, W_ZIP: str, W_TAX: float, W_YTD: float, reply_to: list = None): + __state__ = {} + __state__['w_id'] = w_id + __state__['W_NAME'] = W_NAME + __state__['W_STREET_1'] = W_STREET_1 + __state__['W_STREET_2'] = W_STREET_2 + __state__['W_CITY'] = W_CITY + __state__['W_STATE'] = W_STATE + __state__['W_ZIP'] = W_ZIP + __state__['W_TAX'] = W_TAX + __state__['W_YTD'] = W_YTD + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@warehouse_operator.register +async def get_warehouse(ctx: StatefulFunction, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise WHDoesNotExist(f"Warehouse with key: {ctx.key} does not exist.") + data = { + 'W_NAME': __state__['W_NAME'], 'W_TAX': __state__['W_TAX'], 'W_YTD': __state__['W_YTD'], + 'W_STREET_1': __state__['W_STREET_1'], 'W_STREET_2': __state__['W_STREET_2'], + 'W_CITY': __state__['W_CITY'], 'W_STATE': __state__['W_STATE'], 'W_ZIP': __state__['W_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@warehouse_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise WHDoesNotExist(f"Warehouse with key: {ctx.key} does not exist") + __state__['W_YTD'] = float(__state__['W_YTD']) + h_amount + data = { + 'W_NAME': __state__['W_NAME'], 'W_TAX': __state__['W_TAX'], 'W_YTD': __state__['W_YTD'], + 'W_STREET_1': __state__['W_STREET_1'], 'W_STREET_2': __state__['W_STREET_2'], + 'W_CITY': __state__['W_CITY'], 'W_STATE': __state__['W_STATE'], 'W_ZIP': __state__['W_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +district_operator = Operator('district', n_partitions=4, composite_key_hash_params=(0, ':')) + +@district_operator.register +async def insert(ctx: StatefulFunction, D_ID: int, D_W_ID: int, D_NAME: str, D_STREET_1: str, D_STREET_2: str, + D_CITY: str, D_STATE: str, D_ZIP: str, D_TAX: float, D_YTD: float, + D_NEXT_O_ID: int, reply_to: list = None): + __state__ = {} + __state__['D_ID'] = D_ID + __state__['D_W_ID'] = D_W_ID + __state__['D_NAME'] = D_NAME + __state__['D_STREET_1'] = D_STREET_1 + __state__['D_STREET_2'] = D_STREET_2 + __state__['D_CITY'] = D_CITY + __state__['D_STATE'] = D_STATE + __state__['D_ZIP'] = D_ZIP + __state__['D_TAX'] = D_TAX + __state__['D_YTD'] = D_YTD + __state__['D_NEXT_O_ID'] = D_NEXT_O_ID + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@district_operator.register +async def get_district(ctx: StatefulFunction, w_id: int, d_id: int, c_id: int, + o_entry_d: str, i_ids: list[int], i_qtys: list[int], + i_w_ids: list[int], all_local: bool, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise DistrictDoesNotExist(f"District with key: {ctx.key} does not exist") + d_next_o_id = __state__['D_NEXT_O_ID'] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'order', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(d_next_o_id), params = (w_id, d_id, d_next_o_id, c_id, o_entry_d, None, len(i_ids), all_local, [{'sink': True}])) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'neworder', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(d_next_o_id), params = (w_id, d_id, d_next_o_id, [{'sink': True}])) + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'district', function_name = 'get_district_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'd_id': d_id, 'd_next_o_id': d_next_o_id, 'i_ids': i_ids, 'i_qtys': i_qtys, 'i_w_ids': i_w_ids, 'o_entry_d': o_entry_d, 'w_id': w_id}, None, reply_to)) + +@district_operator.register +async def get_district_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, d_id, d_next_o_id, i_ids, i_qtys, i_w_ids, o_entry_d, w_id) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('d_id'), params.get('d_next_o_id'), params.get('i_ids'), params.get('i_qtys'), params.get('i_w_ids'), params.get('o_entry_d'), params.get('w_id')) + if __loop_index_1 >= len(i_ids): + ctx.call_remote_async(operator_name = 'district', function_name = 'get_district_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'd_id': d_id, 'd_next_o_id': d_next_o_id, 'i_ids': i_ids, 'i_qtys': i_qtys, 'i_w_ids': i_w_ids, 'o_entry_d': o_entry_d, 'w_id': w_id}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + attr_1 = i_ids[i] + reply_to = push_continuation(ctx, reply_to, 'district', 'get_district_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'd_id': d_id, 'd_next_o_id': d_next_o_id, 'i_ids': i_ids, 'i_qtys': i_qtys, 'i_w_ids': i_w_ids, 'o_entry_d': o_entry_d, 'w_id': w_id}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_item', key = attr_1, params = (i, w_id, d_id, o_entry_d, i_qtys[i], i_w_ids[i], d_next_o_id, reply_to)) + +@district_operator.register +async def get_district_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, d_id, d_next_o_id, i_ids, i_qtys, i_w_ids, o_entry_d, w_id) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('d_id'), params.get('d_next_o_id'), params.get('i_ids'), params.get('i_qtys'), params.get('i_w_ids'), params.get('o_entry_d'), params.get('w_id')) + item_replies = _comp_result_1 + __state__['D_NEXT_O_ID'] += 1 + data = { + 'D_ID': __state__['D_ID'], 'D_W_ID': __state__['D_W_ID'], 'D_NAME': __state__['D_NAME'], + 'D_TAX': __state__['D_TAX'], 'D_YTD': __state__['D_YTD'], 'D_NEXT_O_ID': __state__['D_NEXT_O_ID'], + 'D_STREET_1': __state__['D_STREET_1'], 'D_STREET_2': __state__['D_STREET_2'], + 'D_CITY': __state__['D_CITY'], 'D_STATE': __state__['D_STATE'], 'D_ZIP': __state__['D_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, {'district': data, 'items': item_replies}) + +@district_operator.register +async def get_district_step_4(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, d_id, d_next_o_id, i_ids, i_qtys, i_w_ids, o_entry_d, w_id) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('d_id'), params.get('d_next_o_id'), params.get('i_ids'), params.get('i_qtys'), params.get('i_w_ids'), params.get('o_entry_d'), params.get('w_id')) + _comp_result_1.append(attr_2) + ctx.call_remote_async(operator_name = 'district', function_name = 'get_district_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'd_id': d_id, 'd_next_o_id': d_next_o_id, 'i_ids': i_ids, 'i_qtys': i_qtys, 'i_w_ids': i_w_ids, 'o_entry_d': o_entry_d, 'w_id': w_id}, None, reply_to)) + + +@district_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise DistrictDoesNotExist(f"District with key: {ctx.key} does not exist") + __state__['D_YTD'] = float(__state__['D_YTD']) + h_amount + data = { + 'D_ID': __state__['D_ID'], 'D_W_ID': __state__['D_W_ID'], 'D_NAME': __state__['D_NAME'], + 'D_TAX': __state__['D_TAX'], 'D_YTD': __state__['D_YTD'], 'D_NEXT_O_ID': __state__['D_NEXT_O_ID'], + 'D_STREET_1': __state__['D_STREET_1'], 'D_STREET_2': __state__['D_STREET_2'], + 'D_CITY': __state__['D_CITY'], 'D_STATE': __state__['D_STATE'], 'D_ZIP': __state__['D_ZIP'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +item_operator = Operator('item', n_partitions=4) + +@item_operator.register +async def insert(ctx: StatefulFunction, I_ID: int, I_IM_ID: int, I_NAME: str, I_PRICE: float, I_DATA: str, reply_to: list = None): + __state__ = {} + __state__['I_ID'] = I_ID + __state__['I_IM_ID'] = I_IM_ID + __state__['I_NAME'] = I_NAME + __state__['I_PRICE'] = I_PRICE + __state__['I_DATA'] = I_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@item_operator.register +async def get_item(ctx: StatefulFunction, index: int, w_id: int, d_id: int, + o_entry_d: str, i_qty: int, i_w_id: int, d_next_o_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + + if not bool(__state__): + raise TPCCException("Item number is not valid") + attr_1 = __state__['I_DATA'].find("original") + i_brand_generic = attr_1 != -1 + stock = f"{i_w_id}:{ctx.key}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'stock', function_name = 'update_stock', key = stock, params = (index, d_next_o_id, ctx.key, w_id, d_id, i_w_id, o_entry_d, i_qty, __state__['I_NAME'], __state__['I_PRICE'], i_brand_generic, reply_to)) + +customer_operator = Operator('customer', n_partitions=4, composite_key_hash_params=(0, ':')) + +@customer_operator.register +async def insert(ctx: StatefulFunction, C_ID: int, C_D_ID: int, C_W_ID: int, C_FIRST: str, C_MIDDLE: str, + C_LAST: str, C_STREET_1: str, C_STREET_2: str, C_CITY: str, C_STATE: str, + C_ZIP: str, C_PHONE: str, C_SINCE: str, C_CREDIT: str, + C_CREDIT_LIM: float, C_DISCOUNT: float, C_BALANCE: float, + C_YTD_PAYMENT: float, C_PAYMENT_CNT: int, C_DELIVERY_CNT: int, C_DATA: str, reply_to: list = None): + __state__ = {} + __state__['C_ID'] = C_ID + __state__['C_D_ID'] = C_D_ID + __state__['C_W_ID'] = C_W_ID + __state__['C_FIRST'] = C_FIRST + __state__['C_MIDDLE'] = C_MIDDLE + __state__['C_LAST'] = C_LAST + __state__['C_STREET_1'] = C_STREET_1 + __state__['C_STREET_2'] = C_STREET_2 + __state__['C_CITY'] = C_CITY + __state__['C_STATE'] = C_STATE + __state__['C_ZIP'] = C_ZIP + __state__['C_PHONE'] = C_PHONE + __state__['C_SINCE'] = C_SINCE + __state__['C_CREDIT'] = C_CREDIT + __state__['C_CREDIT_LIM'] = C_CREDIT_LIM + __state__['C_DISCOUNT'] = C_DISCOUNT + __state__['C_BALANCE'] = C_BALANCE + __state__['C_YTD_PAYMENT'] = C_YTD_PAYMENT + __state__['C_PAYMENT_CNT'] = C_PAYMENT_CNT + __state__['C_DELIVERY_CNT'] = C_DELIVERY_CNT + __state__['C_DATA'] = C_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@customer_operator.register +async def get_customer(ctx: StatefulFunction, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise CustomerDoesNotExist(f"Customer with id: {ctx.key} does not exist") + data = { + 'C_ID': __state__['C_ID'], 'C_D_ID': __state__['C_D_ID'], 'C_W_ID': __state__['C_W_ID'], + 'C_FIRST': __state__['C_FIRST'], 'C_MIDDLE': __state__['C_MIDDLE'], 'C_LAST': __state__['C_LAST'], + 'C_STREET_1': __state__['C_STREET_1'], 'C_STREET_2': __state__['C_STREET_2'], + 'C_CITY': __state__['C_CITY'], 'C_STATE': __state__['C_STATE'], 'C_ZIP': __state__['C_ZIP'], + 'C_PHONE': __state__['C_PHONE'], 'C_SINCE': __state__['C_SINCE'], 'C_CREDIT': __state__['C_CREDIT'], + 'C_CREDIT_LIM': __state__['C_CREDIT_LIM'], 'C_DISCOUNT': __state__['C_DISCOUNT'], + 'C_BALANCE': __state__['C_BALANCE'], 'C_YTD_PAYMENT': __state__['C_YTD_PAYMENT'], + 'C_PAYMENT_CNT': __state__['C_PAYMENT_CNT'], 'C_DELIVERY_CNT': __state__['C_DELIVERY_CNT'], + 'C_DATA': __state__['C_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@customer_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, d_id: int, w_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise CustomerDoesNotExist(f"Customer with id: {ctx.key} does not exist") + __state__['C_BALANCE'] = float(__state__['C_BALANCE']) - h_amount + __state__['C_YTD_PAYMENT'] = float(__state__['C_YTD_PAYMENT']) + h_amount + __state__['C_PAYMENT_CNT'] = int(__state__['C_PAYMENT_CNT']) + 1 + + if __state__['C_CREDIT'] == "BC": + new_data = f"{__state__['C_ID']} {__state__['C_D_ID']} {__state__['C_W_ID']} {d_id} {w_id} {h_amount}" + __state__['C_DATA'] = (new_data + "|" + __state__['C_DATA']) + + if len(__state__['C_DATA']) > 500: + __state__['C_DATA'] = __state__['C_DATA'][:500] + data = { + 'C_ID': __state__['C_ID'], 'C_D_ID': __state__['C_D_ID'], 'C_W_ID': __state__['C_W_ID'], + 'C_FIRST': __state__['C_FIRST'], 'C_MIDDLE': __state__['C_MIDDLE'], 'C_LAST': __state__['C_LAST'], + 'C_STREET_1': __state__['C_STREET_1'], 'C_STREET_2': __state__['C_STREET_2'], + 'C_CITY': __state__['C_CITY'], 'C_STATE': __state__['C_STATE'], 'C_ZIP': __state__['C_ZIP'], + 'C_PHONE': __state__['C_PHONE'], 'C_SINCE': __state__['C_SINCE'], 'C_CREDIT': __state__['C_CREDIT'], + 'C_CREDIT_LIM': __state__['C_CREDIT_LIM'], 'C_DISCOUNT': __state__['C_DISCOUNT'], + 'C_BALANCE': __state__['C_BALANCE'], 'C_YTD_PAYMENT': __state__['C_YTD_PAYMENT'], + 'C_PAYMENT_CNT': __state__['C_PAYMENT_CNT'], 'C_DELIVERY_CNT': __state__['C_DELIVERY_CNT'], + 'C_DATA': __state__['C_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +customerindex_operator = Operator('customerindex', n_partitions=4, composite_key_hash_params=(0, ':')) + +@customerindex_operator.register +async def insert(ctx: StatefulFunction, C_W_ID: int, C_D_ID: int, C_LAST: str, customers: list[str], reply_to: list = None): + __state__ = {} + __state__['C_W_ID'] = C_W_ID + __state__['C_D_ID'] = C_D_ID + __state__['C_LAST'] = C_LAST + __state__['customers'] = customers + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@customerindex_operator.register +async def pay(ctx: StatefulFunction, h_amount: float, d_id: int, w_id: int, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + index = (len(__state__['customers']) - 1) // 2 + customer = __state__['customers'][index] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'pay', key = customer, params = (h_amount, d_id, w_id, reply_to)) + +stock_operator = Operator('stock', n_partitions=4, composite_key_hash_params=(0, ':')) + +@stock_operator.register +async def insert(ctx: StatefulFunction, S_I_ID: int, S_W_ID: int, S_QUANTITY: int, + S_DIST_01: str, S_DIST_02: str, S_DIST_03: str, S_DIST_04: str, + S_DIST_05: str, S_DIST_06: str, S_DIST_07: str, S_DIST_08: str, + S_DIST_09: str, S_DIST_10: str, S_YTD: int, S_ORDER_CNT: int, + S_REMOTE_CNT: int, S_DATA: str, reply_to: list = None): + __state__ = {} + __state__['S_I_ID'] = S_I_ID + __state__['S_W_ID'] = S_W_ID + __state__['S_QUANTITY'] = S_QUANTITY + __state__['S_DIST_01'] = S_DIST_01 + __state__['S_DIST_02'] = S_DIST_02 + __state__['S_DIST_03'] = S_DIST_03 + __state__['S_DIST_04'] = S_DIST_04 + __state__['S_DIST_05'] = S_DIST_05 + __state__['S_DIST_06'] = S_DIST_06 + __state__['S_DIST_07'] = S_DIST_07 + __state__['S_DIST_08'] = S_DIST_08 + __state__['S_DIST_09'] = S_DIST_09 + __state__['S_DIST_10'] = S_DIST_10 + __state__['S_YTD'] = S_YTD + __state__['S_ORDER_CNT'] = S_ORDER_CNT + __state__['S_REMOTE_CNT'] = S_REMOTE_CNT + __state__['S_DATA'] = S_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@stock_operator.register +async def get_stock(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + data = { + 'S_I_ID': __state__['S_I_ID'].I_ID, 'S_W_ID': __state__['S_W_ID'], 'S_QUANTITY': __state__['S_QUANTITY'], + 'S_DIST_01': __state__['S_DIST_01'], 'S_DIST_02': __state__['S_DIST_02'], 'S_DIST_03': __state__['S_DIST_03'], + 'S_DIST_04': __state__['S_DIST_04'], 'S_DIST_05': __state__['S_DIST_05'], 'S_DIST_06': __state__['S_DIST_06'], + 'S_DIST_07': __state__['S_DIST_07'], 'S_DIST_08': __state__['S_DIST_08'], 'S_DIST_09': __state__['S_DIST_09'], + 'S_DIST_10': __state__['S_DIST_10'], 'S_YTD': __state__['S_YTD'], 'S_ORDER_CNT': __state__['S_ORDER_CNT'], + 'S_REMOTE_CNT': __state__['S_REMOTE_CNT'], 'S_DATA': __state__['S_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + + +@stock_operator.register +async def update_stock(ctx: StatefulFunction, index: int, o_id: int, i_id: int, + w_id: int, d_id: int, i_w_id: int, o_entry_d: str, i_qty: int, + i_name: str, i_price: float, i_brand_generic: bool, reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + + if not bool(__state__): + raise StockDoesNotExist(f"Stock with key: {ctx.key} does not exist") + __state__['S_YTD'] += i_qty + if __state__['S_QUANTITY'] >= i_qty + 10: + __state__['S_QUANTITY'] -= i_qty + else: + __state__['S_QUANTITY'] = __state__['S_QUANTITY'] + 91 - i_qty + __state__['S_ORDER_CNT'] += 1 + + if i_w_id != w_id: + __state__['S_REMOTE_CNT'] += 1 + + if i_brand_generic: + if "original" in __state__['S_DATA']: + brand_generic = "B" + else: + brand_generic = "G" + else: + brand_generic = "G" + ol_amount = i_qty * i_price + dist = ( + __state__['S_DIST_01'], + __state__['S_DIST_02'], + __state__['S_DIST_03'], + __state__['S_DIST_04'], + __state__['S_DIST_05'], + __state__['S_DIST_06'], + __state__['S_DIST_07'], + __state__['S_DIST_08'], + __state__['S_DIST_09'], + __state__['S_DIST_10'], + ) + s_dist_xx = dist[d_id - 1] + ol_number = index + 1 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'orderline', function_name = 'insert', key = str(w_id) + ":" + str(d_id) + ":" + str(o_id) + ":" + str(ol_number), params = (w_id, d_id, o_id, i_id, ol_number, i_qty, o_entry_d, i_w_id, s_dist_xx, ol_amount, [{'sink': True}])) + ctx.put(__state__) + return send_reply(ctx, reply_to, { + 'i_name': i_name, + 'i_price': i_price, + 'ol_amount': ol_amount, + 's_quantity': __state__['S_QUANTITY'], + 'brand_generic': brand_generic, + }) + +history_operator = Operator('history', n_partitions=4, composite_key_hash_params=(0, ':')) + +@history_operator.register +async def insert(ctx: StatefulFunction, H_C_ID: int, H_C_D_ID: int, H_C_W_ID: int, + H_D_ID: int, H_W_ID: int, H_DATE: str, H_AMOUNT: float, H_DATA: str, reply_to: list = None): + __state__ = {} + __state__['H_C_ID'] = H_C_ID + __state__['H_C_D_ID'] = H_C_D_ID + __state__['H_C_W_ID'] = H_C_W_ID + __state__['H_D_ID'] = H_D_ID + __state__['H_W_ID'] = H_W_ID + __state__['H_DATE'] = H_DATE + __state__['H_AMOUNT'] = H_AMOUNT + __state__['H_DATA'] = H_DATA + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@history_operator.register +async def get_history(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise HistoryDoesNotExist(f"History with key: {ctx.key} does not exist") + data = { + 'H_C_ID': __state__['H_C_ID'], 'H_C_D_ID': __state__['H_C_D_ID'], 'H_C_W_ID': __state__['H_C_W_ID'], + 'H_D_ID': __state__['H_D_ID'], 'H_W_ID': __state__['H_W_ID'], 'H_DATE': __state__['H_DATE'], + 'H_AMOUNT': __state__['H_AMOUNT'], 'H_DATA': __state__['H_DATA'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +order_operator = Operator('order', n_partitions=4, composite_key_hash_params=(0, ':')) + +@order_operator.register +async def insert(ctx: StatefulFunction, O_W_ID: int, O_D_ID: int, O_ID: int, O_C_ID: int = 0, O_ENTRY_D: str = "", O_CARRIER_ID: Optional[int] = None, O_OL_CNT: int = 0, O_ALL_LOCAL: bool = True, reply_to: list = None): + __state__ = {} + __state__['O_W_ID'] = O_W_ID + __state__['O_D_ID'] = O_D_ID + __state__['O_ID'] = O_ID + __state__['O_C_ID'] = O_C_ID + __state__['O_ENTRY_D'] = O_ENTRY_D + __state__['O_CARRIER_ID'] = O_CARRIER_ID + __state__['O_OL_CNT'] = O_OL_CNT + __state__['O_ALL_LOCAL'] = O_ALL_LOCAL + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@order_operator.register +async def get_order(ctx: StatefulFunction, c_id: int, entry_d: str, ol_cnt: int, all_local: bool, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + data = { + 'O_W_ID': __state__['O_W_ID'], 'O_D_ID': __state__['O_D_ID'], 'O_ID': __state__['O_ID'], 'O_C_ID': c_id, 'O_ENTRY_D': entry_d, + 'O_OL_CNT': ol_cnt, 'O_ALL_LOCAL': all_local, + } + if not bool(__state__): + raise OrderDoesNotExist(f"Order with key: {ctx.key} does not exist") + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +neworder_operator = Operator('neworder', n_partitions=4, composite_key_hash_params=(0, ':')) + +@neworder_operator.register +async def insert(ctx: StatefulFunction, NO_W_ID: int, NO_D_ID: int, NO_O_ID: int, reply_to: list = None): + __state__ = {} + __state__['NO_W_ID'] = NO_W_ID + __state__['NO_D_ID'] = NO_D_ID + __state__['NO_O_ID'] = NO_O_ID + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@neworder_operator.register +async def create(ctx: StatefulFunction, no_o_id: int, no_d_id: int, no_w_id: int, reply_to: list = None) -> None: + __state__ = ctx.get() or {} + __state__['NO_O_ID'] = no_o_id + __state__['NO_D_ID'] = no_d_id + __state__['NO_W_ID'] = no_w_id + ctx.put(__state__) + +orderline_operator = Operator('orderline', n_partitions=4, composite_key_hash_params=(0, ':')) + +@orderline_operator.register +async def insert( + ctx: StatefulFunction, OL_W_ID: int, + OL_D_ID: int, + OL_O_ID: int, + OL_I_ID: int, + OL_NUMBER: int, + OL_QUANTITY: int = 0, + OL_DELIVERY_D: Optional[str] = None, + OL_SUPPLY_W_ID: Optional[int] = None, + OL_DIST_INFO: str = "", + OL_AMOUNT: float = 0.0, +reply_to: list = None): + __state__ = {} + __state__['OL_W_ID'] = OL_W_ID + __state__['OL_D_ID'] = OL_D_ID + __state__['OL_O_ID'] = OL_O_ID + __state__['OL_I_ID'] = OL_I_ID + __state__['OL_NUMBER'] = OL_NUMBER + __state__['OL_QUANTITY'] = OL_QUANTITY + __state__['OL_DELIVERY_D'] = OL_DELIVERY_D + __state__['OL_SUPPLY_W_ID'] = OL_SUPPLY_W_ID + __state__['OL_DIST_INFO'] = OL_DIST_INFO + __state__['OL_AMOUNT'] = OL_AMOUNT + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@orderline_operator.register +async def get_order_line(ctx: StatefulFunction, reply_to: list = None) -> dict: + __state__ = ctx.get() or {} + if not bool(__state__): + raise OrderLineDoesNotExist(f"OrderLine with key: {ctx.key} does not exist") + data = { + 'OL_W_ID': __state__['OL_W_ID'], 'OL_D_ID': __state__['OL_D_ID'], 'OL_O_ID': __state__['OL_O_ID'], + 'OL_I_ID': __state__['OL_I_ID'].I_ID, 'OL_NUMBER': __state__['OL_NUMBER'], 'OL_QUANTITY': __state__['OL_QUANTITY'], + 'OL_DELIVERY_D': __state__['OL_DELIVERY_D'], 'OL_SUPPLY_W_ID': __state__['OL_SUPPLY_W_ID'] if __state__['OL_SUPPLY_W_ID'] else None, + 'OL_DIST_INFO': __state__['OL_DIST_INFO'], 'OL_AMOUNT': __state__['OL_AMOUNT'], + } + ctx.put(__state__) + return send_reply(ctx, reply_to, data) + +newordertxn_operator = Operator('newordertxn', n_partitions=4) + +@newordertxn_operator.register +async def insert(ctx: StatefulFunction, txn_id: str, reply_to: list = None): + __state__ = {} + __state__['txn_id'] = txn_id + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@newordertxn_operator.register +async def new_order(ctx: StatefulFunction, params: dict, reply_to: list = None) -> str: + w_id: int = params["W_ID"] + d_id: int = params["D_ID"] + c_id: int = params["C_ID"] + o_entry_d: str = params["O_ENTRY_D"] + i_ids: list[int] = params["I_IDS"] + i_w_ids: list[int] = params["I_W_IDS"] + i_qtys: list[int] = params["I_QTYS"] + assert len(i_ids) > 0 + assert len(i_ids) == len(i_w_ids) == len(i_qtys) + all_local = True + for item_w_id in i_w_ids: + if item_w_id != w_id: + all_local = False + break + district = f"{w_id}:{d_id}" + customer = f"{w_id}:{d_id}:{c_id}" + reply_to = push_continuation(ctx, reply_to, 'newordertxn', 'new_order_step_2', ctx.key, {'all_local': all_local, 'c_id': c_id, 'customer': customer, 'd_id': d_id, 'district': district, 'i_ids': i_ids, 'i_qtys': i_qtys, 'i_w_ids': i_w_ids, 'o_entry_d': o_entry_d, 'w_id': w_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_warehouse', key = w_id, params = (reply_to,)) + +@newordertxn_operator.register +async def new_order_step_2(ctx: StatefulFunction, func_context, warehouse_data = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (all_local, c_id, customer, d_id, district, i_ids, i_qtys, i_w_ids, o_entry_d, w_id) = (params.get('all_local'), params.get('c_id'), params.get('customer'), params.get('d_id'), params.get('district'), params.get('i_ids'), params.get('i_qtys'), params.get('i_w_ids'), params.get('o_entry_d'), params.get('w_id')) + reply_to = push_continuation(ctx, reply_to, 'newordertxn', 'new_order_step_3', ctx.key, {'customer': customer, 'o_entry_d': o_entry_d, 'warehouse_data': warehouse_data}) + ctx.call_remote_async(operator_name = 'district', function_name = 'get_district', key = district, params = (w_id, d_id, c_id, o_entry_d, i_ids, i_qtys, i_w_ids, all_local, reply_to)) + +@newordertxn_operator.register +async def new_order_step_3(ctx: StatefulFunction, func_context, district_bundle = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, o_entry_d, warehouse_data) = (params.get('customer'), params.get('o_entry_d'), params.get('warehouse_data')) + reply_to = push_continuation(ctx, reply_to, 'newordertxn', 'new_order_step_4', ctx.key, {'district_bundle': district_bundle, 'o_entry_d': o_entry_d, 'warehouse_data': warehouse_data}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_customer', key = customer, params = (reply_to,)) + +@newordertxn_operator.register +async def new_order_step_4(ctx: StatefulFunction, func_context, customer_data = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (district_bundle, o_entry_d, warehouse_data) = (params.get('district_bundle'), params.get('o_entry_d'), params.get('warehouse_data')) + district_data = district_bundle['district'] + item_replies = district_bundle['items'] + total = sum(item_reply['ol_amount'] for item_reply in item_replies) + w_tax: float = warehouse_data['W_TAX'] + d_tax: float = district_data['D_TAX'] + total = total * (1 - customer_data['C_DISCOUNT']) * (1 + w_tax + d_tax) + o_id = district_data['D_NEXT_O_ID'] + attr_4 = ";" + item_str = attr_4.join( + f"{r['i_name']},{r['s_quantity']},{r['brand_generic']},{r['i_price']:.2f},{r['ol_amount']:.2f}" + for r in item_replies + ) + return send_reply(ctx, reply_to, ( + f"NO|C_ID={customer_data['C_ID']},C_LAST={customer_data['C_LAST']}," + f"C_CREDIT={customer_data['C_CREDIT']}," + f"C_DISCOUNT={customer_data['C_DISCOUNT']:.4f},W_TAX={w_tax:.4f},D_TAX={d_tax:.4f}," + f"O_ID={o_id},O_ENTRY_D={o_entry_d},N_ITEMS={len(item_replies)}," + f"TOTAL={total:.2f},ITEMS=[{item_str}]" + )) + +paymenttxn_operator = Operator('paymenttxn', n_partitions=4) + +@paymenttxn_operator.register +async def insert( + ctx: StatefulFunction, txn_id: str, + w_id: int, + c_w_id: int, + d_id: int = 0, + c_d_id: int = 0, + h_amount: float = 0.0, + h_date: str = "", +reply_to: list = None): + __state__ = {} + __state__['txn_id'] = txn_id + __state__['W_ID'] = w_id + __state__['D_ID'] = d_id + __state__['C_W_ID'] = c_w_id + __state__['C_D_ID'] = c_d_id + __state__['C_ID'] = None + __state__['H_AMOUNT'] = h_amount + __state__['H_DATE'] = h_date + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + + +@paymenttxn_operator.register +async def get_customer_data(ctx: StatefulFunction, c_last: Optional[str], reply_to: list = None) -> Dict: + __state__ = ctx.get() or {} + if __state__['C_ID'] is not None: + customer = f"{__state__['C_W_ID']}:{__state__['C_D_ID']}:{__state__['C_ID']}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'pay', key = customer, params = (__state__['H_AMOUNT'], __state__['D_ID'], __state__['W_ID'], reply_to)) + else: + customer_idx = f"{__state__['C_W_ID']}:{__state__['C_D_ID']}:{c_last}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customerindex', function_name = 'pay', key = customer_idx, params = (__state__['H_AMOUNT'], __state__['D_ID'], __state__['W_ID'], reply_to)) + + +@paymenttxn_operator.register +async def payment(ctx: StatefulFunction, params: dict, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + w_id: int = params["W_ID"] + d_id: int = int(params["D_ID"]) + h_amount: float = params["H_AMOUNT"] + c_w_id: int = params["C_W_ID"] + c_d_id: int = int(params["C_D_ID"]) + attr_1 = params.get("C_ID") + c_id: Optional[int] = int(params["C_ID"]) if attr_1 is not None else None + attr_2 = params.get("C_LAST") + c_last: Optional[str] = attr_2 + h_date: str = params["H_DATE"] + __state__['W_ID'] = w_id + __state__['D_ID'] = d_id + __state__['C_ID'] = c_id + __state__['C_W_ID'] = c_w_id + __state__['C_D_ID'] = c_d_id + __state__['H_DATE'] = h_date + __state__['H_AMOUNT'] = h_amount + district = f"{w_id}:{d_id}" + reply_to = push_continuation(ctx, reply_to, 'paymenttxn', 'payment_step_2', ctx.key, {'district': district, 'h_amount': h_amount, 'w_id': w_id}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'paymenttxn', function_name = 'get_customer_data', key = ctx.key, params = (c_last, reply_to)) + +@paymenttxn_operator.register +async def payment_step_2(ctx: StatefulFunction, func_context, customer_data = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (district, h_amount, w_id) = (params.get('district'), params.get('h_amount'), params.get('w_id')) + reply_to = push_continuation(ctx, reply_to, 'paymenttxn', 'payment_step_3', ctx.key, {'customer_data': customer_data, 'h_amount': h_amount, 'w_id': w_id}) + ctx.call_remote_async(operator_name = 'district', function_name = 'pay', key = district, params = (h_amount, reply_to)) + +@paymenttxn_operator.register +async def payment_step_3(ctx: StatefulFunction, func_context, district_data = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer_data, h_amount, w_id) = (params.get('customer_data'), params.get('h_amount'), params.get('w_id')) + reply_to = push_continuation(ctx, reply_to, 'paymenttxn', 'payment_step_4', ctx.key, {'customer_data': customer_data, 'district_data': district_data}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'pay', key = w_id, params = (h_amount, reply_to)) + +@paymenttxn_operator.register +async def payment_step_4(ctx: StatefulFunction, func_context, warehouse_data = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer_data, district_data) = (params.get('customer_data'), params.get('district_data')) + h_data = f"{warehouse_data['W_NAME']} {district_data['D_NAME']}" + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'history', function_name = 'insert', key = str(__state__['W_ID']) + ":" + str(__state__['D_ID']) + ":" + str(customer_data['C_ID']), params = (customer_data['C_ID'], __state__['C_D_ID'], __state__['C_W_ID'], __state__['D_ID'], __state__['W_ID'], __state__['H_DATE'], __state__['H_AMOUNT'], h_data, [{'sink': True}])) + + if customer_data['C_CREDIT'] == "BC": + c_data_str = f",C_DATA={customer_data['C_DATA'][:200]}" + else: + c_data_str = "" + ctx.put(__state__) + return send_reply(ctx, reply_to, ( + f"P|W_ID={__state__['W_ID']},D_ID={district_data['D_ID']},C_ID={customer_data['C_ID']}," + f"C_D_ID={customer_data['C_D_ID']},C_W_ID={customer_data['C_W_ID']}," + f"C_NAME={customer_data['C_FIRST']} {customer_data['C_MIDDLE']} {customer_data['C_LAST']}," + f"C_BAL={customer_data['C_BALANCE']:.2f},C_DISCOUNT={customer_data['C_DISCOUNT']:.4f}," + f"C_CREDIT={customer_data['C_CREDIT']},W_TAX={warehouse_data['W_TAX']:.4f}," + f"D_TAX={district_data['D_TAX']:.4f},H_AMOUNT={__state__['H_AMOUNT']:.2f}," + f"H_DATE={__state__['H_DATE']}{c_data_str}" + )) + diff --git a/obol/examples/benchmarks/compiled/ycsb.py b/obol/examples/benchmarks/compiled/ycsb.py new file mode 100644 index 00000000..29f34451 --- /dev/null +++ b/obol/examples/benchmarks/compiled/ycsb.py @@ -0,0 +1,143 @@ +import uuid +from styx.common.operator import Operator +from styx.common.stateful_function import StatefulFunction +from styx.common.logging import logging + +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None + +from obol.core import entity, send_async + +class NotEnoughCredit(Exception): + pass +ycsb_operator = Operator('ycsb', n_partitions=4) + +@ycsb_operator.register +async def insert(ctx: StatefulFunction, key: str, reply_to: list = None): + __state__ = {} + __state__['key'] = key + __state__['value'] = 1_000_000 + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@ycsb_operator.register +async def get_key(ctx: StatefulFunction, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['key']) + + +@ycsb_operator.register +async def get_value(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['value']) + + +@ycsb_operator.register +async def set_value(ctx: StatefulFunction, value: int, reply_to: list = None): + __state__ = ctx.get() or {} + __state__['value'] = value + ctx.put(__state__) + + +@ycsb_operator.register +async def read(ctx: StatefulFunction, reply_to: list = None) -> tuple[str, int]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, (ctx.key, __state__['value'])) + + +@ycsb_operator.register +async def update(ctx: StatefulFunction, reply_to: list = None) -> tuple[str, int]: + __state__ = ctx.get() or {} + __state__['value'] += 1 + ctx.put(__state__) + return send_reply(ctx, reply_to, (ctx.key, __state__['value'])) + + + +@ycsb_operator.register +async def transfer(ctx: StatefulFunction, key_b: str, reply_to: list = None) -> tuple[str, int]: + __state__ = ctx.get() or {} + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'ycsb', function_name = 'update', key = key_b, params = ([{'sink': True}],)) + __state__['value'] -= 1 + if __state__['value'] < 0: + raise NotEnoughCredit(f"Not enough credit for user: {ctx.key}") + ctx.put(__state__) + return send_reply(ctx, reply_to, (ctx.key, __state__['value'])) + diff --git a/obol/examples/benchmarks/figures/tpcc_results.png b/obol/examples/benchmarks/figures/tpcc_results.png new file mode 100644 index 00000000..50cbbdd1 Binary files /dev/null and b/obol/examples/benchmarks/figures/tpcc_results.png differ diff --git a/obol/examples/benchmarks/figures/ycsb_results.png b/obol/examples/benchmarks/figures/ycsb_results.png new file mode 100644 index 00000000..e0c4d636 Binary files /dev/null and b/obol/examples/benchmarks/figures/ycsb_results.png differ diff --git a/obol/examples/benchmarks/original/tpcc.py b/obol/examples/benchmarks/original/tpcc.py new file mode 100644 index 00000000..dd8fe5d5 --- /dev/null +++ b/obol/examples/benchmarks/original/tpcc.py @@ -0,0 +1,679 @@ +from __future__ import annotations +from typing import Any, Dict, Optional +import datetime +from obol.api import entity, send_async, get_entity_by_key, gather, exists + + +# ────────────────────────────────────────── +# Exceptions +# ────────────────────────────────────────── + +class InsufficientStock(Exception): + pass + +class InvalidItem(Exception): + pass + +class WHDoesNotExist(Exception): + pass + +class DistrictDoesNotExist(Exception): + pass + +class TPCCException(Exception): + pass + +class CustomerDoesNotExist(Exception): + pass + +class HistoryDoesNotExist(Exception): + pass + +class StockDoesNotExist(Exception): + pass + +class OrderDoesNotExist(Exception): + pass + +class OrderLineDoesNotExist(Exception): + pass + + +# ────────────────────────────────────────── +# Entity: Warehouse +# Key: w_id (int) +# ────────────────────────────────────────── + +@entity +class Warehouse: + def __init__(self, w_id: int, W_NAME: str, W_STREET_1: str, W_STREET_2: str, + W_CITY: str, W_STATE: str, W_ZIP: str, W_TAX: float, W_YTD: float): + self.w_id: int = w_id + self.W_NAME: str = W_NAME + self.W_STREET_1: str = W_STREET_1 + self.W_STREET_2: str = W_STREET_2 + self.W_CITY: str = W_CITY + self.W_STATE: str = W_STATE + self.W_ZIP: str = W_ZIP + self.W_TAX: float = W_TAX + self.W_YTD: float = W_YTD + + def __key__(self) -> int: + return self.w_id + + def get_warehouse(self) -> Dict: + if not exists(self): + raise WHDoesNotExist(f"Warehouse with key: {self} does not exist.") + data = { + 'W_NAME': self.W_NAME, 'W_TAX': self.W_TAX, 'W_YTD': self.W_YTD, + 'W_STREET_1': self.W_STREET_1, 'W_STREET_2': self.W_STREET_2, + 'W_CITY': self.W_CITY, 'W_STATE': self.W_STATE, 'W_ZIP': self.W_ZIP, + } + return data + + def pay(self, h_amount: float) -> Dict: + if not exists(self): + raise WHDoesNotExist(f"Warehouse with key: {self} does not exist") + self.W_YTD = float(self.W_YTD) + h_amount + data = { + 'W_NAME': self.W_NAME, 'W_TAX': self.W_TAX, 'W_YTD': self.W_YTD, + 'W_STREET_1': self.W_STREET_1, 'W_STREET_2': self.W_STREET_2, + 'W_CITY': self.W_CITY, 'W_STATE': self.W_STATE, 'W_ZIP': self.W_ZIP, + } + return data + + +# ────────────────────────────────────────── +# Entity: District +# Key: (D_W_ID, D_ID) → partitioned on D_W_ID (Warehouse) +# ────────────────────────────────────────── + +@entity +class District: + def __init__(self, D_ID: int, D_W_ID: Warehouse, D_NAME: str, D_STREET_1: str, D_STREET_2: str, + D_CITY: str, D_STATE: str, D_ZIP: str, D_TAX: float, D_YTD: float, + D_NEXT_O_ID: int): + self.D_ID: int = D_ID + self.D_W_ID: Warehouse = D_W_ID + self.D_NAME: str = D_NAME + self.D_STREET_1: str = D_STREET_1 + self.D_STREET_2: str = D_STREET_2 + self.D_CITY: str = D_CITY + self.D_STATE: str = D_STATE + self.D_ZIP: str = D_ZIP + self.D_TAX: float = D_TAX + self.D_YTD: float = D_YTD + self.D_NEXT_O_ID: int = D_NEXT_O_ID + + def __key__(self) -> tuple[Warehouse, int]: + return (self.D_W_ID, self.D_ID) + + def get_district(self, w_id: Warehouse, d_id: int, c_id: int, + o_entry_d: str, i_ids: list[Item], i_qtys: list[int], + i_w_ids: list[Warehouse], all_local: bool) -> Dict: + if not exists(self): + raise DistrictDoesNotExist(f"District with key: {self} does not exist") + + d_next_o_id = self.D_NEXT_O_ID + + # Create the Order and NewOrder records (fire-and-forget side effects) + send_async(Order(w_id, d_id, d_next_o_id, c_id, o_entry_d, None, len(i_ids), all_local)) + send_async(NewOrder(w_id, d_id, d_next_o_id)) + + item_replies = gather(*[ + i_ids[i].get_item(i, w_id, d_id, o_entry_d, i_qtys[i], i_w_ids[i], d_next_o_id) + for i in range(len(i_ids)) + ]) + + self.D_NEXT_O_ID += 1 + + + data = { + 'D_ID': self.D_ID, 'D_W_ID': self.D_W_ID, 'D_NAME': self.D_NAME, + 'D_TAX': self.D_TAX, 'D_YTD': self.D_YTD, 'D_NEXT_O_ID': self.D_NEXT_O_ID, + 'D_STREET_1': self.D_STREET_1, 'D_STREET_2': self.D_STREET_2, + 'D_CITY': self.D_CITY, 'D_STATE': self.D_STATE, 'D_ZIP': self.D_ZIP, + } + + return {'district': data, 'items': item_replies} + + def pay(self, h_amount: float) -> Dict: + if not exists(self): + raise DistrictDoesNotExist(f"District with key: {self} does not exist") + + self.D_YTD = float(self.D_YTD) + h_amount + data = { + 'D_ID': self.D_ID, 'D_W_ID': self.D_W_ID, 'D_NAME': self.D_NAME, + 'D_TAX': self.D_TAX, 'D_YTD': self.D_YTD, 'D_NEXT_O_ID': self.D_NEXT_O_ID, + 'D_STREET_1': self.D_STREET_1, 'D_STREET_2': self.D_STREET_2, + 'D_CITY': self.D_CITY, 'D_STATE': self.D_STATE, 'D_ZIP': self.D_ZIP, + } + return data + + +# ────────────────────────────────────────── +# Entity: Item +# Key: I_ID (int) +# ────────────────────────────────────────── + +@entity +class Item: + def __init__(self, I_ID: int, I_IM_ID: int, I_NAME: str, I_PRICE: float, I_DATA: str): + self.I_ID: int = I_ID + self.I_IM_ID: int = I_IM_ID + self.I_NAME: str = I_NAME + self.I_PRICE: float = I_PRICE + self.I_DATA: str = I_DATA + + def __key__(self) -> int: + return self.I_ID + + def get_item(self, index: int, w_id: Warehouse, d_id: int, + o_entry_d: str, i_qty: int, i_w_id: Warehouse, d_next_o_id: int) -> Dict: + + if not exists(self): + raise TPCCException("Item number is not valid") + + i_brand_generic = self.I_DATA.find("original") != -1 + + stock = get_entity_by_key(Stock, (i_w_id, self)) + + stock_reply = stock.update_stock( + index, d_next_o_id, self, w_id, d_id, i_w_id, + o_entry_d, i_qty, self.I_NAME, self.I_PRICE, i_brand_generic + ) + + return stock_reply + + +# ────────────────────────────────────────── +# Entity: Customer +# Key: (C_W_ID, C_D_ID, C_ID) +# ────────────────────────────────────────── + +@entity +class Customer: + def __init__(self, C_ID: int, C_D_ID: int, C_W_ID: Warehouse, C_FIRST: str, C_MIDDLE: str, + C_LAST: str, C_STREET_1: str, C_STREET_2: str, C_CITY: str, C_STATE: str, + C_ZIP: str, C_PHONE: str, C_SINCE: str, C_CREDIT: str, + C_CREDIT_LIM: float, C_DISCOUNT: float, C_BALANCE: float, + C_YTD_PAYMENT: float, C_PAYMENT_CNT: int, C_DELIVERY_CNT: int, C_DATA: str): + self.C_ID: int = C_ID + self.C_D_ID: int = C_D_ID + self.C_W_ID: Warehouse = C_W_ID + self.C_FIRST: str = C_FIRST + self.C_MIDDLE: str = C_MIDDLE + self.C_LAST: str = C_LAST + self.C_STREET_1: str = C_STREET_1 + self.C_STREET_2: str = C_STREET_2 + self.C_CITY: str = C_CITY + self.C_STATE: str = C_STATE + self.C_ZIP: str = C_ZIP + self.C_PHONE: str = C_PHONE + self.C_SINCE: str = C_SINCE + self.C_CREDIT: str = C_CREDIT + self.C_CREDIT_LIM: float = C_CREDIT_LIM + self.C_DISCOUNT: float = C_DISCOUNT + self.C_BALANCE: float = C_BALANCE + self.C_YTD_PAYMENT: float = C_YTD_PAYMENT + self.C_PAYMENT_CNT: int = C_PAYMENT_CNT + self.C_DELIVERY_CNT: int = C_DELIVERY_CNT + self.C_DATA: str = C_DATA + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.C_W_ID, self.C_D_ID, self.C_ID) + + def get_customer(self) -> Dict: + if not exists(self): + raise CustomerDoesNotExist(f"Customer with id: {self} does not exist") + data = { + 'C_ID': self.C_ID, 'C_D_ID': self.C_D_ID, 'C_W_ID': self.C_W_ID, + 'C_FIRST': self.C_FIRST, 'C_MIDDLE': self.C_MIDDLE, 'C_LAST': self.C_LAST, + 'C_STREET_1': self.C_STREET_1, 'C_STREET_2': self.C_STREET_2, + 'C_CITY': self.C_CITY, 'C_STATE': self.C_STATE, 'C_ZIP': self.C_ZIP, + 'C_PHONE': self.C_PHONE, 'C_SINCE': self.C_SINCE, 'C_CREDIT': self.C_CREDIT, + 'C_CREDIT_LIM': self.C_CREDIT_LIM, 'C_DISCOUNT': self.C_DISCOUNT, + 'C_BALANCE': self.C_BALANCE, 'C_YTD_PAYMENT': self.C_YTD_PAYMENT, + 'C_PAYMENT_CNT': self.C_PAYMENT_CNT, 'C_DELIVERY_CNT': self.C_DELIVERY_CNT, + 'C_DATA': self.C_DATA, + } + return data + + def pay(self, h_amount: float, d_id: int, w_id: Warehouse) -> Dict: + if not exists(self): + raise CustomerDoesNotExist(f"Customer with id: {self} does not exist") + + self.C_BALANCE = float(self.C_BALANCE) - h_amount + self.C_YTD_PAYMENT = float(self.C_YTD_PAYMENT) + h_amount + self.C_PAYMENT_CNT = int(self.C_PAYMENT_CNT) + 1 + + if self.C_CREDIT == "BC": + new_data = f"{self.C_ID} {self.C_D_ID} {self.C_W_ID} {d_id} {w_id} {h_amount}" + self.C_DATA = (new_data + "|" + self.C_DATA) + + if len(self.C_DATA) > 500: + self.C_DATA = self.C_DATA[:500] + + data = { + 'C_ID': self.C_ID, 'C_D_ID': self.C_D_ID, 'C_W_ID': self.C_W_ID, + 'C_FIRST': self.C_FIRST, 'C_MIDDLE': self.C_MIDDLE, 'C_LAST': self.C_LAST, + 'C_STREET_1': self.C_STREET_1, 'C_STREET_2': self.C_STREET_2, + 'C_CITY': self.C_CITY, 'C_STATE': self.C_STATE, 'C_ZIP': self.C_ZIP, + 'C_PHONE': self.C_PHONE, 'C_SINCE': self.C_SINCE, 'C_CREDIT': self.C_CREDIT, + 'C_CREDIT_LIM': self.C_CREDIT_LIM, 'C_DISCOUNT': self.C_DISCOUNT, + 'C_BALANCE': self.C_BALANCE, 'C_YTD_PAYMENT': self.C_YTD_PAYMENT, + 'C_PAYMENT_CNT': self.C_PAYMENT_CNT, 'C_DELIVERY_CNT': self.C_DELIVERY_CNT, + 'C_DATA': self.C_DATA, + } + return data + + +# ────────────────────────────────────────── +# Entity: CustomerIndex +# ────────────────────────────────────────── + +@entity +class CustomerIndex: + def __init__(self, C_W_ID: Warehouse, C_D_ID: int, C_LAST: str, customers: list[Customer]): + self.C_W_ID: Warehouse = C_W_ID + self.C_D_ID: int = C_D_ID + self.C_LAST: str = C_LAST + self.customers: list[Customer] = customers + + def __key__(self) -> tuple[Warehouse, int, str]: + return (self.C_W_ID, self.C_D_ID, self.C_LAST) + + def pay(self, h_amount: float, d_id: int, w_id: Warehouse) -> Dict: + index = (len(self.customers) - 1) // 2 + customer = self.customers[index] + return customer.pay(h_amount, d_id, w_id) + + +# ────────────────────────────────────────── +# Entity: Stock +# Key: (S_W_ID, S_I_ID) +# ────────────────────────────────────────── + +@entity +class Stock: + def __init__(self, S_I_ID: Item, S_W_ID: Warehouse, S_QUANTITY: int, + S_DIST_01: str, S_DIST_02: str, S_DIST_03: str, S_DIST_04: str, + S_DIST_05: str, S_DIST_06: str, S_DIST_07: str, S_DIST_08: str, + S_DIST_09: str, S_DIST_10: str, S_YTD: int, S_ORDER_CNT: int, + S_REMOTE_CNT: int, S_DATA: str): + self.S_I_ID: Item = S_I_ID + self.S_W_ID: Warehouse = S_W_ID + self.S_QUANTITY: int = S_QUANTITY + self.S_DIST_01: str = S_DIST_01 + self.S_DIST_02: str = S_DIST_02 + self.S_DIST_03: str = S_DIST_03 + self.S_DIST_04: str = S_DIST_04 + self.S_DIST_05: str = S_DIST_05 + self.S_DIST_06: str = S_DIST_06 + self.S_DIST_07: str = S_DIST_07 + self.S_DIST_08: str = S_DIST_08 + self.S_DIST_09: str = S_DIST_09 + self.S_DIST_10: str = S_DIST_10 + self.S_YTD: int = S_YTD + self.S_ORDER_CNT: int = S_ORDER_CNT + self.S_REMOTE_CNT: int = S_REMOTE_CNT + self.S_DATA: str = S_DATA + + def __key__(self) -> tuple[Warehouse, Item]: + return (self.S_W_ID, self.S_I_ID) + + def get_stock(self) -> dict: + data = { + 'S_I_ID': self.S_I_ID.I_ID, 'S_W_ID': self.S_W_ID, 'S_QUANTITY': self.S_QUANTITY, + 'S_DIST_01': self.S_DIST_01, 'S_DIST_02': self.S_DIST_02, 'S_DIST_03': self.S_DIST_03, + 'S_DIST_04': self.S_DIST_04, 'S_DIST_05': self.S_DIST_05, 'S_DIST_06': self.S_DIST_06, + 'S_DIST_07': self.S_DIST_07, 'S_DIST_08': self.S_DIST_08, 'S_DIST_09': self.S_DIST_09, + 'S_DIST_10': self.S_DIST_10, 'S_YTD': self.S_YTD, 'S_ORDER_CNT': self.S_ORDER_CNT, + 'S_REMOTE_CNT': self.S_REMOTE_CNT, 'S_DATA': self.S_DATA, + } + return data + + def update_stock(self, index: int, o_id: int, i_id: Item, + w_id: Warehouse, d_id: int, i_w_id: Warehouse, o_entry_d: str, i_qty: int, + i_name: str, i_price: float, i_brand_generic: bool) -> Dict: + + if not exists(self): + raise StockDoesNotExist(f"Stock with key: {self} does not exist") + + self.S_YTD += i_qty + if self.S_QUANTITY >= i_qty + 10: + self.S_QUANTITY -= i_qty + else: + self.S_QUANTITY = self.S_QUANTITY + 91 - i_qty + self.S_ORDER_CNT += 1 + + if i_w_id != w_id: + self.S_REMOTE_CNT += 1 + + if i_brand_generic: + if "original" in self.S_DATA: + brand_generic = "B" + else: + brand_generic = "G" + else: + brand_generic = "G" + + ol_amount = i_qty * i_price + + dist = ( + self.S_DIST_01, + self.S_DIST_02, + self.S_DIST_03, + self.S_DIST_04, + self.S_DIST_05, + self.S_DIST_06, + self.S_DIST_07, + self.S_DIST_08, + self.S_DIST_09, + self.S_DIST_10, + ) + s_dist_xx = dist[d_id - 1] + ol_number = index + 1 + + send_async(OrderLine( + OL_W_ID=w_id, + OL_D_ID=d_id, + OL_O_ID=o_id, + OL_I_ID=i_id, + OL_NUMBER=ol_number, + OL_QUANTITY=i_qty, + OL_DELIVERY_D=o_entry_d, + OL_SUPPLY_W_ID=i_w_id, + OL_DIST_INFO=s_dist_xx, + OL_AMOUNT=ol_amount, + )) + + return { + 'i_name': i_name, + 'i_price': i_price, + 'ol_amount': ol_amount, + 's_quantity': self.S_QUANTITY, + 'brand_generic': brand_generic, + } + + +# ────────────────────────────────────────── +# Entity: History +# ────────────────────────────────────────── + +@entity +class History: + def __init__(self, H_C_ID: int, H_C_D_ID: int, H_C_W_ID: Warehouse, + H_D_ID: int, H_W_ID: Warehouse, H_DATE: str, H_AMOUNT: float, H_DATA: str): + self.H_C_ID: int = H_C_ID + self.H_C_D_ID: int = H_C_D_ID + self.H_C_W_ID: Warehouse = H_C_W_ID + self.H_D_ID: int = H_D_ID + self.H_W_ID: Warehouse = H_W_ID + self.H_DATE: str = H_DATE + self.H_AMOUNT: float = H_AMOUNT + self.H_DATA: str = H_DATA + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.H_W_ID, self.H_D_ID, self.H_C_ID) + + def get_history(self) -> dict: + if not exists(self): + raise HistoryDoesNotExist(f"History with key: {self} does not exist") + + data = { + 'H_C_ID': self.H_C_ID, 'H_C_D_ID': self.H_C_D_ID, 'H_C_W_ID': self.H_C_W_ID, + 'H_D_ID': self.H_D_ID, 'H_W_ID': self.H_W_ID, 'H_DATE': self.H_DATE, + 'H_AMOUNT': self.H_AMOUNT, 'H_DATA': self.H_DATA, + } + return data + + +# ────────────────────────────────────────── +# Entity: Order +# ────────────────────────────────────────── + +@entity +class Order: + def __init__(self, O_W_ID: Warehouse, O_D_ID: int, O_ID: int, O_C_ID: int = 0, O_ENTRY_D: str = "", O_CARRIER_ID: Optional[int] = None, O_OL_CNT: int = 0, O_ALL_LOCAL: bool = True): + self.O_W_ID: Warehouse = O_W_ID + self.O_D_ID: int = O_D_ID + self.O_ID: int = O_ID + self.O_C_ID: int = O_C_ID + self.O_ENTRY_D: str = O_ENTRY_D + self.O_CARRIER_ID: Optional[int] = O_CARRIER_ID + self.O_OL_CNT: int = O_OL_CNT + self.O_ALL_LOCAL: bool = O_ALL_LOCAL + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.O_W_ID, self.O_D_ID, self.O_ID) + + def get_order(self, c_id: int, entry_d: str, ol_cnt: int, all_local: bool) -> dict: + data = { + 'O_W_ID': self.O_W_ID, 'O_D_ID': self.O_D_ID, 'O_ID': self.O_ID, 'O_C_ID': c_id, 'O_ENTRY_D': entry_d, + 'O_OL_CNT': ol_cnt, 'O_ALL_LOCAL': all_local, + } + if not exists(self): + raise OrderDoesNotExist(f"Order with key: {self} does not exist") + + return data + + +# ────────────────────────────────────────── +# Entity: NewOrder +# ────────────────────────────────────────── + +@entity +class NewOrder: + def __init__(self, NO_W_ID: Warehouse, NO_D_ID: int, NO_O_ID: int): + self.NO_W_ID: Warehouse = NO_W_ID + self.NO_D_ID: int = NO_D_ID + self.NO_O_ID: int = NO_O_ID + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.NO_W_ID, self.NO_D_ID, self.NO_O_ID) + + def create(self, no_o_id: int, no_d_id: int, no_w_id: Warehouse) -> None: + self.NO_O_ID = no_o_id + self.NO_D_ID = no_d_id + self.NO_W_ID = no_w_id + + +# ────────────────────────────────────────── +# Entity: OrderLine +# ────────────────────────────────────────── + +@entity +class OrderLine: + def __init__( + self, + OL_W_ID: Warehouse, + OL_D_ID: int, + OL_O_ID: int, + OL_I_ID: Item, + OL_NUMBER: int, + OL_QUANTITY: int = 0, + OL_DELIVERY_D: Optional[str] = None, + OL_SUPPLY_W_ID: Optional[Warehouse] = None, + OL_DIST_INFO: str = "", + OL_AMOUNT: float = 0.0 + ): + self.OL_W_ID = OL_W_ID + self.OL_D_ID = OL_D_ID + self.OL_O_ID = OL_O_ID + self.OL_I_ID = OL_I_ID + self.OL_NUMBER = OL_NUMBER + self.OL_QUANTITY = OL_QUANTITY + self.OL_DELIVERY_D = OL_DELIVERY_D + self.OL_SUPPLY_W_ID = OL_SUPPLY_W_ID + self.OL_DIST_INFO = OL_DIST_INFO + self.OL_AMOUNT = OL_AMOUNT + + def __key__(self) -> tuple[Warehouse, int, int, int]: + return (self.OL_W_ID, self.OL_D_ID, self.OL_O_ID, self.OL_NUMBER) + + def get_order_line(self) -> dict: + if not exists(self): + raise OrderLineDoesNotExist(f"OrderLine with key: {self} does not exist") + data = { + 'OL_W_ID': self.OL_W_ID, 'OL_D_ID': self.OL_D_ID, 'OL_O_ID': self.OL_O_ID, + 'OL_I_ID': self.OL_I_ID.I_ID, 'OL_NUMBER': self.OL_NUMBER, 'OL_QUANTITY': self.OL_QUANTITY, + 'OL_DELIVERY_D': self.OL_DELIVERY_D, 'OL_SUPPLY_W_ID': self.OL_SUPPLY_W_ID if self.OL_SUPPLY_W_ID else None, + 'OL_DIST_INFO': self.OL_DIST_INFO, 'OL_AMOUNT': self.OL_AMOUNT, + } + return data + + +# ────────────────────────────────────────── +# Entity: NewOrderTxn (transaction coordinator) +# ────────────────────────────────────────── + +@entity +class NewOrderTxn: + def __init__(self, txn_id: str): + self.txn_id: str = txn_id + + def __key__(self) -> str: + return self.txn_id + + def new_order(self, params: dict) -> str: + + w_id: Warehouse = params["W_ID"] + d_id: int = params["D_ID"] + c_id: int = params["C_ID"] + o_entry_d: str = params["O_ENTRY_D"] + i_ids: list[Item] = params["I_IDS"] + i_w_ids: list[Warehouse] = params["I_W_IDS"] + i_qtys: list[int] = params["I_QTYS"] + + assert len(i_ids) > 0 + assert len(i_ids) == len(i_w_ids) == len(i_qtys) + + all_local = True + for item_w_id in i_w_ids: + if item_w_id != w_id: + all_local = False + break + + + district = get_entity_by_key(District, (w_id, d_id)) + customer = get_entity_by_key(Customer, (w_id, d_id, c_id)) + + + warehouse_data, district_bundle, customer_data = gather( + w_id.get_warehouse(), + district.get_district(w_id, d_id, c_id, o_entry_d, i_ids, i_qtys, i_w_ids, all_local), + customer.get_customer(), + ) + district_data = district_bundle['district'] + item_replies = district_bundle['items'] + + total = sum(item_reply['ol_amount'] for item_reply in item_replies) + + # Pack the final response. + w_tax: float = warehouse_data['W_TAX'] + d_tax: float = district_data['D_TAX'] + total = total * (1 - customer_data['C_DISCOUNT']) * (1 + w_tax + d_tax) + o_id = district_data['D_NEXT_O_ID'] + + item_str = ";".join( + f"{r['i_name']},{r['s_quantity']},{r['brand_generic']},{r['i_price']:.2f},{r['ol_amount']:.2f}" + for r in item_replies + ) + + return ( + f"NO|C_ID={customer_data['C_ID']},C_LAST={customer_data['C_LAST']}," + f"C_CREDIT={customer_data['C_CREDIT']}," + f"C_DISCOUNT={customer_data['C_DISCOUNT']:.4f},W_TAX={w_tax:.4f},D_TAX={d_tax:.4f}," + f"O_ID={o_id},O_ENTRY_D={o_entry_d},N_ITEMS={len(item_replies)}," + f"TOTAL={total:.2f},ITEMS=[{item_str}]" + ) + + +# ────────────────────────────────────────── +# Entity: PaymentTxn (transaction coordinator) +# ────────────────────────────────────────── + +@entity +class PaymentTxn: + def __init__( + self, + txn_id: str, + w_id: Warehouse, + c_w_id: Warehouse, + d_id: int = 0, + c_d_id: int = 0, + h_amount: float = 0.0, + h_date: str = "", + ): + self.txn_id: str = txn_id + self.W_ID: Warehouse = w_id + self.D_ID: int = d_id + self.C_W_ID: Warehouse = c_w_id + self.C_D_ID: int = c_d_id + self.C_ID: Optional[int] = None + self.H_AMOUNT: float = h_amount + self.H_DATE: str = h_date + + def __key__(self) -> str: + return self.txn_id + + + def get_customer_data(self, c_last: Optional[str]) -> Dict: + if self.C_ID is not None: + customer = get_entity_by_key( + Customer, (self.C_W_ID, self.C_D_ID, self.C_ID) + ) + return customer.pay(self.H_AMOUNT, self.D_ID, self.W_ID) + else: + customer_idx = get_entity_by_key( + CustomerIndex, (self.C_W_ID, self.C_D_ID, c_last) + ) + return customer_idx.pay(self.H_AMOUNT, self.D_ID, self.W_ID) + + def payment(self, params: dict) -> str: + w_id: Warehouse = params["W_ID"] + d_id: int = int(params["D_ID"]) + h_amount: float = params["H_AMOUNT"] + c_w_id: Warehouse = params["C_W_ID"] + c_d_id: int = int(params["C_D_ID"]) + + c_id: Optional[int] = int(params["C_ID"]) if params.get("C_ID") is not None else None + c_last: Optional[str] = params.get("C_LAST") + h_date: str = params["H_DATE"] + + self.W_ID = w_id + self.D_ID = d_id + self.C_ID = c_id + self.C_W_ID = c_w_id + self.C_D_ID = c_d_id + self.H_DATE = h_date + self.H_AMOUNT = h_amount + + district = get_entity_by_key(District, (w_id, d_id)) + + customer_data, district_data, warehouse_data = gather( + self.get_customer_data(c_last), district.pay(h_amount), w_id.pay(h_amount) + ) + + # Build history record and persist it. + h_data = f"{warehouse_data['W_NAME']} {district_data['D_NAME']}" + send_async(History( + customer_data['C_ID'], self.C_D_ID, self.C_W_ID, + self.D_ID, self.W_ID, self.H_DATE, self.H_AMOUNT, h_data + )) + + if customer_data['C_CREDIT'] == "BC": + c_data_str = f",C_DATA={customer_data['C_DATA'][:200]}" + else: + c_data_str = "" + + return ( + f"P|W_ID={self.W_ID},D_ID={district_data['D_ID']},C_ID={customer_data['C_ID']}," + f"C_D_ID={customer_data['C_D_ID']},C_W_ID={customer_data['C_W_ID']}," + f"C_NAME={customer_data['C_FIRST']} {customer_data['C_MIDDLE']} {customer_data['C_LAST']}," + f"C_BAL={customer_data['C_BALANCE']:.2f},C_DISCOUNT={customer_data['C_DISCOUNT']:.4f}," + f"C_CREDIT={customer_data['C_CREDIT']},W_TAX={warehouse_data['W_TAX']:.4f}," + f"D_TAX={district_data['D_TAX']:.4f},H_AMOUNT={self.H_AMOUNT:.2f}," + f"H_DATE={self.H_DATE}{c_data_str}" + ) diff --git a/obol/examples/benchmarks/original/tpcc_no_gather.py b/obol/examples/benchmarks/original/tpcc_no_gather.py new file mode 100644 index 00000000..7f906ef3 --- /dev/null +++ b/obol/examples/benchmarks/original/tpcc_no_gather.py @@ -0,0 +1,677 @@ +from __future__ import annotations +from typing import Any, Dict, Optional +import datetime +from obol.api import entity, send_async, get_entity_by_key, gather, exists + + +# ────────────────────────────────────────── +# Exceptions +# ────────────────────────────────────────── + +class InsufficientStock(Exception): + pass + +class InvalidItem(Exception): + pass + +class WHDoesNotExist(Exception): + pass + +class DistrictDoesNotExist(Exception): + pass + +class TPCCException(Exception): + pass + +class CustomerDoesNotExist(Exception): + pass + +class HistoryDoesNotExist(Exception): + pass + +class StockDoesNotExist(Exception): + pass + +class OrderDoesNotExist(Exception): + pass + +class OrderLineDoesNotExist(Exception): + pass + + +# ────────────────────────────────────────── +# Entity: Warehouse +# Key: w_id (int) +# ────────────────────────────────────────── + +@entity +class Warehouse: + def __init__(self, w_id: int, W_NAME: str, W_STREET_1: str, W_STREET_2: str, + W_CITY: str, W_STATE: str, W_ZIP: str, W_TAX: float, W_YTD: float): + self.w_id: int = w_id + self.W_NAME: str = W_NAME + self.W_STREET_1: str = W_STREET_1 + self.W_STREET_2: str = W_STREET_2 + self.W_CITY: str = W_CITY + self.W_STATE: str = W_STATE + self.W_ZIP: str = W_ZIP + self.W_TAX: float = W_TAX + self.W_YTD: float = W_YTD + + def __key__(self) -> int: + return self.w_id + + def get_warehouse(self) -> Dict: + if not exists(self): + raise WHDoesNotExist(f"Warehouse with key: {self} does not exist.") + data = { + 'W_NAME': self.W_NAME, 'W_TAX': self.W_TAX, 'W_YTD': self.W_YTD, + 'W_STREET_1': self.W_STREET_1, 'W_STREET_2': self.W_STREET_2, + 'W_CITY': self.W_CITY, 'W_STATE': self.W_STATE, 'W_ZIP': self.W_ZIP, + } + return data + + def pay(self, h_amount: float) -> Dict: + if not exists(self): + raise WHDoesNotExist(f"Warehouse with key: {self} does not exist") + self.W_YTD = float(self.W_YTD) + h_amount + data = { + 'W_NAME': self.W_NAME, 'W_TAX': self.W_TAX, 'W_YTD': self.W_YTD, + 'W_STREET_1': self.W_STREET_1, 'W_STREET_2': self.W_STREET_2, + 'W_CITY': self.W_CITY, 'W_STATE': self.W_STATE, 'W_ZIP': self.W_ZIP, + } + return data + + +# ────────────────────────────────────────── +# Entity: District +# Key: (D_W_ID, D_ID) → partitioned on D_W_ID (Warehouse) +# ────────────────────────────────────────── + +@entity +class District: + def __init__(self, D_ID: int, D_W_ID: Warehouse, D_NAME: str, D_STREET_1: str, D_STREET_2: str, + D_CITY: str, D_STATE: str, D_ZIP: str, D_TAX: float, D_YTD: float, + D_NEXT_O_ID: int): + self.D_ID: int = D_ID + self.D_W_ID: Warehouse = D_W_ID + self.D_NAME: str = D_NAME + self.D_STREET_1: str = D_STREET_1 + self.D_STREET_2: str = D_STREET_2 + self.D_CITY: str = D_CITY + self.D_STATE: str = D_STATE + self.D_ZIP: str = D_ZIP + self.D_TAX: float = D_TAX + self.D_YTD: float = D_YTD + self.D_NEXT_O_ID: int = D_NEXT_O_ID + + def __key__(self) -> tuple[Warehouse, int]: + return (self.D_W_ID, self.D_ID) + + def get_district(self, w_id: Warehouse, d_id: int, c_id: int, + o_entry_d: str, i_ids: list[Item], i_qtys: list[int], + i_w_ids: list[Warehouse], all_local: bool) -> Dict: + if not exists(self): + raise DistrictDoesNotExist(f"District with key: {self} does not exist") + + d_next_o_id = self.D_NEXT_O_ID + + # Create the Order and NewOrder records (fire-and-forget side effects) + send_async(Order(w_id, d_id, d_next_o_id, c_id, o_entry_d, None, len(i_ids), all_local)) + send_async(NewOrder(w_id, d_id, d_next_o_id)) + + item_replies = [ + i_ids[i].get_item(i, w_id, d_id, o_entry_d, i_qtys[i], i_w_ids[i], d_next_o_id) + for i in range(len(i_ids)) + ] + + self.D_NEXT_O_ID += 1 + + data = { + 'D_ID': self.D_ID, 'D_W_ID': self.D_W_ID, 'D_NAME': self.D_NAME, + 'D_TAX': self.D_TAX, 'D_YTD': self.D_YTD, 'D_NEXT_O_ID': self.D_NEXT_O_ID, + 'D_STREET_1': self.D_STREET_1, 'D_STREET_2': self.D_STREET_2, + 'D_CITY': self.D_CITY, 'D_STATE': self.D_STATE, 'D_ZIP': self.D_ZIP, + } + + return {'district': data, 'items': item_replies} + + def pay(self, h_amount: float) -> Dict: + if not exists(self): + raise DistrictDoesNotExist(f"District with key: {self} does not exist") + + self.D_YTD = float(self.D_YTD) + h_amount + data = { + 'D_ID': self.D_ID, 'D_W_ID': self.D_W_ID, 'D_NAME': self.D_NAME, + 'D_TAX': self.D_TAX, 'D_YTD': self.D_YTD, 'D_NEXT_O_ID': self.D_NEXT_O_ID, + 'D_STREET_1': self.D_STREET_1, 'D_STREET_2': self.D_STREET_2, + 'D_CITY': self.D_CITY, 'D_STATE': self.D_STATE, 'D_ZIP': self.D_ZIP, + } + return data + + +# ────────────────────────────────────────── +# Entity: Item +# Key: I_ID (int) +# ────────────────────────────────────────── + +@entity +class Item: + def __init__(self, I_ID: int, I_IM_ID: int, I_NAME: str, I_PRICE: float, I_DATA: str): + self.I_ID: int = I_ID + self.I_IM_ID: int = I_IM_ID + self.I_NAME: str = I_NAME + self.I_PRICE: float = I_PRICE + self.I_DATA: str = I_DATA + + def __key__(self) -> int: + return self.I_ID + + def get_item(self, index: int, w_id: Warehouse, d_id: int, + o_entry_d: str, i_qty: int, i_w_id: Warehouse, d_next_o_id: int) -> Dict: + + if not exists(self): + raise TPCCException("Item number is not valid") + + i_brand_generic = self.I_DATA.find("original") != -1 + + stock = get_entity_by_key(Stock, (i_w_id, self)) + + stock_reply = stock.update_stock( + index, d_next_o_id, self, w_id, d_id, i_w_id, + o_entry_d, i_qty, self.I_NAME, self.I_PRICE, i_brand_generic + ) + + return stock_reply + + +# ────────────────────────────────────────── +# Entity: Customer +# Key: (C_W_ID, C_D_ID, C_ID) +# ────────────────────────────────────────── + +@entity +class Customer: + def __init__(self, C_ID: int, C_D_ID: int, C_W_ID: Warehouse, C_FIRST: str, C_MIDDLE: str, + C_LAST: str, C_STREET_1: str, C_STREET_2: str, C_CITY: str, C_STATE: str, + C_ZIP: str, C_PHONE: str, C_SINCE: str, C_CREDIT: str, + C_CREDIT_LIM: float, C_DISCOUNT: float, C_BALANCE: float, + C_YTD_PAYMENT: float, C_PAYMENT_CNT: int, C_DELIVERY_CNT: int, C_DATA: str): + self.C_ID: int = C_ID + self.C_D_ID: int = C_D_ID + self.C_W_ID: Warehouse = C_W_ID + self.C_FIRST: str = C_FIRST + self.C_MIDDLE: str = C_MIDDLE + self.C_LAST: str = C_LAST + self.C_STREET_1: str = C_STREET_1 + self.C_STREET_2: str = C_STREET_2 + self.C_CITY: str = C_CITY + self.C_STATE: str = C_STATE + self.C_ZIP: str = C_ZIP + self.C_PHONE: str = C_PHONE + self.C_SINCE: str = C_SINCE + self.C_CREDIT: str = C_CREDIT + self.C_CREDIT_LIM: float = C_CREDIT_LIM + self.C_DISCOUNT: float = C_DISCOUNT + self.C_BALANCE: float = C_BALANCE + self.C_YTD_PAYMENT: float = C_YTD_PAYMENT + self.C_PAYMENT_CNT: int = C_PAYMENT_CNT + self.C_DELIVERY_CNT: int = C_DELIVERY_CNT + self.C_DATA: str = C_DATA + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.C_W_ID, self.C_D_ID, self.C_ID) + + def get_customer(self) -> Dict: + if not exists(self): + raise CustomerDoesNotExist(f"Customer with id: {self} does not exist") + data = { + 'C_ID': self.C_ID, 'C_D_ID': self.C_D_ID, 'C_W_ID': self.C_W_ID, + 'C_FIRST': self.C_FIRST, 'C_MIDDLE': self.C_MIDDLE, 'C_LAST': self.C_LAST, + 'C_STREET_1': self.C_STREET_1, 'C_STREET_2': self.C_STREET_2, + 'C_CITY': self.C_CITY, 'C_STATE': self.C_STATE, 'C_ZIP': self.C_ZIP, + 'C_PHONE': self.C_PHONE, 'C_SINCE': self.C_SINCE, 'C_CREDIT': self.C_CREDIT, + 'C_CREDIT_LIM': self.C_CREDIT_LIM, 'C_DISCOUNT': self.C_DISCOUNT, + 'C_BALANCE': self.C_BALANCE, 'C_YTD_PAYMENT': self.C_YTD_PAYMENT, + 'C_PAYMENT_CNT': self.C_PAYMENT_CNT, 'C_DELIVERY_CNT': self.C_DELIVERY_CNT, + 'C_DATA': self.C_DATA, + } + return data + + def pay(self, h_amount: float, d_id: int, w_id: Warehouse) -> Dict: + if not exists(self): + raise CustomerDoesNotExist(f"Customer with id: {self} does not exist") + + self.C_BALANCE = float(self.C_BALANCE) - h_amount + self.C_YTD_PAYMENT = float(self.C_YTD_PAYMENT) + h_amount + self.C_PAYMENT_CNT = int(self.C_PAYMENT_CNT) + 1 + + if self.C_CREDIT == "BC": + new_data = f"{self.C_ID} {self.C_D_ID} {self.C_W_ID} {d_id} {w_id} {h_amount}" + self.C_DATA = (new_data + "|" + self.C_DATA) + + if len(self.C_DATA) > 500: + self.C_DATA = self.C_DATA[:500] + + data = { + 'C_ID': self.C_ID, 'C_D_ID': self.C_D_ID, 'C_W_ID': self.C_W_ID, + 'C_FIRST': self.C_FIRST, 'C_MIDDLE': self.C_MIDDLE, 'C_LAST': self.C_LAST, + 'C_STREET_1': self.C_STREET_1, 'C_STREET_2': self.C_STREET_2, + 'C_CITY': self.C_CITY, 'C_STATE': self.C_STATE, 'C_ZIP': self.C_ZIP, + 'C_PHONE': self.C_PHONE, 'C_SINCE': self.C_SINCE, 'C_CREDIT': self.C_CREDIT, + 'C_CREDIT_LIM': self.C_CREDIT_LIM, 'C_DISCOUNT': self.C_DISCOUNT, + 'C_BALANCE': self.C_BALANCE, 'C_YTD_PAYMENT': self.C_YTD_PAYMENT, + 'C_PAYMENT_CNT': self.C_PAYMENT_CNT, 'C_DELIVERY_CNT': self.C_DELIVERY_CNT, + 'C_DATA': self.C_DATA, + } + return data + + +# ────────────────────────────────────────── +# Entity: CustomerIndex +# ────────────────────────────────────────── + +@entity +class CustomerIndex: + def __init__(self, C_W_ID: Warehouse, C_D_ID: int, C_LAST: str, customers: list[Customer]): + self.C_W_ID: Warehouse = C_W_ID + self.C_D_ID: int = C_D_ID + self.C_LAST: str = C_LAST + self.customers: list[Customer] = customers + + def __key__(self) -> tuple[Warehouse, int, str]: + return (self.C_W_ID, self.C_D_ID, self.C_LAST) + + def pay(self, h_amount: float, d_id: int, w_id: Warehouse) -> Dict: + index = (len(self.customers) - 1) // 2 + customer = self.customers[index] + return customer.pay(h_amount, d_id, w_id) + + +# ────────────────────────────────────────── +# Entity: Stock +# Key: (S_W_ID, S_I_ID) +# ────────────────────────────────────────── + +@entity +class Stock: + def __init__(self, S_I_ID: Item, S_W_ID: Warehouse, S_QUANTITY: int, + S_DIST_01: str, S_DIST_02: str, S_DIST_03: str, S_DIST_04: str, + S_DIST_05: str, S_DIST_06: str, S_DIST_07: str, S_DIST_08: str, + S_DIST_09: str, S_DIST_10: str, S_YTD: int, S_ORDER_CNT: int, + S_REMOTE_CNT: int, S_DATA: str): + self.S_I_ID: Item = S_I_ID + self.S_W_ID: Warehouse = S_W_ID + self.S_QUANTITY: int = S_QUANTITY + self.S_DIST_01: str = S_DIST_01 + self.S_DIST_02: str = S_DIST_02 + self.S_DIST_03: str = S_DIST_03 + self.S_DIST_04: str = S_DIST_04 + self.S_DIST_05: str = S_DIST_05 + self.S_DIST_06: str = S_DIST_06 + self.S_DIST_07: str = S_DIST_07 + self.S_DIST_08: str = S_DIST_08 + self.S_DIST_09: str = S_DIST_09 + self.S_DIST_10: str = S_DIST_10 + self.S_YTD: int = S_YTD + self.S_ORDER_CNT: int = S_ORDER_CNT + self.S_REMOTE_CNT: int = S_REMOTE_CNT + self.S_DATA: str = S_DATA + + def __key__(self) -> tuple[Warehouse, Item]: + return (self.S_W_ID, self.S_I_ID) + + def get_stock(self) -> dict: + data = { + 'S_I_ID': self.S_I_ID.I_ID, 'S_W_ID': self.S_W_ID, 'S_QUANTITY': self.S_QUANTITY, + 'S_DIST_01': self.S_DIST_01, 'S_DIST_02': self.S_DIST_02, 'S_DIST_03': self.S_DIST_03, + 'S_DIST_04': self.S_DIST_04, 'S_DIST_05': self.S_DIST_05, 'S_DIST_06': self.S_DIST_06, + 'S_DIST_07': self.S_DIST_07, 'S_DIST_08': self.S_DIST_08, 'S_DIST_09': self.S_DIST_09, + 'S_DIST_10': self.S_DIST_10, 'S_YTD': self.S_YTD, 'S_ORDER_CNT': self.S_ORDER_CNT, + 'S_REMOTE_CNT': self.S_REMOTE_CNT, 'S_DATA': self.S_DATA, + } + return data + + def update_stock(self, index: int, o_id: int, i_id: Item, + w_id: Warehouse, d_id: int, i_w_id: Warehouse, o_entry_d: str, i_qty: int, + i_name: str, i_price: float, i_brand_generic: bool) -> Dict: + + if not exists(self): + raise StockDoesNotExist(f"Stock with key: {self} does not exist") + + self.S_YTD += i_qty + if self.S_QUANTITY >= i_qty + 10: + self.S_QUANTITY -= i_qty + else: + self.S_QUANTITY = self.S_QUANTITY + 91 - i_qty + self.S_ORDER_CNT += 1 + + if i_w_id != w_id: + self.S_REMOTE_CNT += 1 + + if i_brand_generic: + if "original" in self.S_DATA: + brand_generic = "B" + else: + brand_generic = "G" + else: + brand_generic = "G" + + ol_amount = i_qty * i_price + + dist = ( + self.S_DIST_01, + self.S_DIST_02, + self.S_DIST_03, + self.S_DIST_04, + self.S_DIST_05, + self.S_DIST_06, + self.S_DIST_07, + self.S_DIST_08, + self.S_DIST_09, + self.S_DIST_10, + ) + s_dist_xx = dist[d_id - 1] + ol_number = index + 1 + + send_async(OrderLine( + OL_W_ID=w_id, + OL_D_ID=d_id, + OL_O_ID=o_id, + OL_I_ID=i_id, + OL_NUMBER=ol_number, + OL_QUANTITY=i_qty, + OL_DELIVERY_D=o_entry_d, + OL_SUPPLY_W_ID=i_w_id, + OL_DIST_INFO=s_dist_xx, + OL_AMOUNT=ol_amount, + )) + + return { + 'i_name': i_name, + 'i_price': i_price, + 'ol_amount': ol_amount, + 's_quantity': self.S_QUANTITY, + 'brand_generic': brand_generic, + } + + +# ────────────────────────────────────────── +# Entity: History +# ────────────────────────────────────────── + +@entity +class History: + def __init__(self, H_C_ID: int, H_C_D_ID: int, H_C_W_ID: Warehouse, + H_D_ID: int, H_W_ID: Warehouse, H_DATE: str, H_AMOUNT: float, H_DATA: str): + self.H_C_ID: int = H_C_ID + self.H_C_D_ID: int = H_C_D_ID + self.H_C_W_ID: Warehouse = H_C_W_ID + self.H_D_ID: int = H_D_ID + self.H_W_ID: Warehouse = H_W_ID + self.H_DATE: str = H_DATE + self.H_AMOUNT: float = H_AMOUNT + self.H_DATA: str = H_DATA + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.H_W_ID, self.H_D_ID, self.H_C_ID) + + def get_history(self) -> dict: + if not exists(self): + raise HistoryDoesNotExist(f"History with key: {self} does not exist") + + data = { + 'H_C_ID': self.H_C_ID, 'H_C_D_ID': self.H_C_D_ID, 'H_C_W_ID': self.H_C_W_ID, + 'H_D_ID': self.H_D_ID, 'H_W_ID': self.H_W_ID, 'H_DATE': self.H_DATE, + 'H_AMOUNT': self.H_AMOUNT, 'H_DATA': self.H_DATA, + } + return data + + +# ────────────────────────────────────────── +# Entity: Order +# ────────────────────────────────────────── + +@entity +class Order: + def __init__(self, O_W_ID: Warehouse, O_D_ID: int, O_ID: int, O_C_ID: int = 0, O_ENTRY_D: str = "", O_CARRIER_ID: Optional[int] = None, O_OL_CNT: int = 0, O_ALL_LOCAL: bool = True): + self.O_W_ID: Warehouse = O_W_ID + self.O_D_ID: int = O_D_ID + self.O_ID: int = O_ID + self.O_C_ID: int = O_C_ID + self.O_ENTRY_D: str = O_ENTRY_D + self.O_CARRIER_ID: Optional[int] = O_CARRIER_ID + self.O_OL_CNT: int = O_OL_CNT + self.O_ALL_LOCAL: bool = O_ALL_LOCAL + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.O_W_ID, self.O_D_ID, self.O_ID) + + def get_order(self, c_id: int, entry_d: str, ol_cnt: int, all_local: bool) -> dict: + data = { + 'O_W_ID': self.O_W_ID, 'O_D_ID': self.O_D_ID, 'O_ID': self.O_ID, 'O_C_ID': c_id, 'O_ENTRY_D': entry_d, + 'O_OL_CNT': ol_cnt, 'O_ALL_LOCAL': all_local, + } + if not exists(self): + raise OrderDoesNotExist(f"Order with key: {self} does not exist") + + return data + + +# ────────────────────────────────────────── +# Entity: NewOrder +# ────────────────────────────────────────── + +@entity +class NewOrder: + def __init__(self, NO_W_ID: Warehouse, NO_D_ID: int, NO_O_ID: int): + self.NO_W_ID: Warehouse = NO_W_ID + self.NO_D_ID: int = NO_D_ID + self.NO_O_ID: int = NO_O_ID + + def __key__(self) -> tuple[Warehouse, int, int]: + return (self.NO_W_ID, self.NO_D_ID, self.NO_O_ID) + + def create(self, no_o_id: int, no_d_id: int, no_w_id: Warehouse) -> None: + self.NO_O_ID = no_o_id + self.NO_D_ID = no_d_id + self.NO_W_ID = no_w_id + + +# ────────────────────────────────────────── +# Entity: OrderLine +# ────────────────────────────────────────── + +@entity +class OrderLine: + def __init__( + self, + OL_W_ID: Warehouse, + OL_D_ID: int, + OL_O_ID: int, + OL_I_ID: Item, + OL_NUMBER: int, + OL_QUANTITY: int = 0, + OL_DELIVERY_D: Optional[str] = None, + OL_SUPPLY_W_ID: Optional[Warehouse] = None, + OL_DIST_INFO: str = "", + OL_AMOUNT: float = 0.0 + ): + self.OL_W_ID = OL_W_ID + self.OL_D_ID = OL_D_ID + self.OL_O_ID = OL_O_ID + self.OL_I_ID = OL_I_ID + self.OL_NUMBER = OL_NUMBER + self.OL_QUANTITY = OL_QUANTITY + self.OL_DELIVERY_D = OL_DELIVERY_D + self.OL_SUPPLY_W_ID = OL_SUPPLY_W_ID + self.OL_DIST_INFO = OL_DIST_INFO + self.OL_AMOUNT = OL_AMOUNT + + def __key__(self) -> tuple[Warehouse, int, int, int]: + return (self.OL_W_ID, self.OL_D_ID, self.OL_O_ID, self.OL_NUMBER) + + def get_order_line(self) -> dict: + if not exists(self): + raise OrderLineDoesNotExist(f"OrderLine with key: {self} does not exist") + data = { + 'OL_W_ID': self.OL_W_ID, 'OL_D_ID': self.OL_D_ID, 'OL_O_ID': self.OL_O_ID, + 'OL_I_ID': self.OL_I_ID.I_ID, 'OL_NUMBER': self.OL_NUMBER, 'OL_QUANTITY': self.OL_QUANTITY, + 'OL_DELIVERY_D': self.OL_DELIVERY_D, 'OL_SUPPLY_W_ID': self.OL_SUPPLY_W_ID if self.OL_SUPPLY_W_ID else None, + 'OL_DIST_INFO': self.OL_DIST_INFO, 'OL_AMOUNT': self.OL_AMOUNT, + } + return data + + +# ────────────────────────────────────────── +# Entity: NewOrderTxn (transaction coordinator) +# ────────────────────────────────────────── + +@entity +class NewOrderTxn: + def __init__(self, txn_id: str): + self.txn_id: str = txn_id + + def __key__(self) -> str: + return self.txn_id + + def new_order(self, params: dict) -> str: + + w_id: Warehouse = params["W_ID"] + d_id: int = params["D_ID"] + c_id: int = params["C_ID"] + o_entry_d: str = params["O_ENTRY_D"] + i_ids: list[Item] = params["I_IDS"] + i_w_ids: list[Warehouse] = params["I_W_IDS"] + i_qtys: list[int] = params["I_QTYS"] + + assert len(i_ids) > 0 + assert len(i_ids) == len(i_w_ids) == len(i_qtys) + + all_local = True + for item_w_id in i_w_ids: + if item_w_id != w_id: + all_local = False + break + + + district = get_entity_by_key(District, (w_id, d_id)) + customer = get_entity_by_key(Customer, (w_id, d_id, c_id)) + + + warehouse_data = w_id.get_warehouse() + district_bundle = district.get_district(w_id, d_id, c_id, o_entry_d, i_ids, i_qtys, i_w_ids, all_local) + customer_data = customer.get_customer() + + district_data = district_bundle['district'] + item_replies = district_bundle['items'] + + total = sum(item_reply['ol_amount'] for item_reply in item_replies) + + # Pack the final response. + w_tax: float = warehouse_data['W_TAX'] + d_tax: float = district_data['D_TAX'] + total = total * (1 - customer_data['C_DISCOUNT']) * (1 + w_tax + d_tax) + o_id = district_data['D_NEXT_O_ID'] + + item_str = ";".join( + f"{r['i_name']},{r['s_quantity']},{r['brand_generic']},{r['i_price']:.2f},{r['ol_amount']:.2f}" + for r in item_replies + ) + + return ( + f"NO|C_ID={customer_data['C_ID']},C_LAST={customer_data['C_LAST']}," + f"C_CREDIT={customer_data['C_CREDIT']}," + f"C_DISCOUNT={customer_data['C_DISCOUNT']:.4f},W_TAX={w_tax:.4f},D_TAX={d_tax:.4f}," + f"O_ID={o_id},O_ENTRY_D={o_entry_d},N_ITEMS={len(item_replies)}," + f"TOTAL={total:.2f},ITEMS=[{item_str}]" + ) + + +# ────────────────────────────────────────── +# Entity: PaymentTxn (transaction coordinator) +# ────────────────────────────────────────── + +@entity +class PaymentTxn: + def __init__( + self, + txn_id: str, + w_id: Warehouse, + c_w_id: Warehouse, + d_id: int = 0, + c_d_id: int = 0, + h_amount: float = 0.0, + h_date: str = "", + ): + self.txn_id: str = txn_id + self.W_ID: Warehouse = w_id + self.D_ID: int = d_id + self.C_W_ID: Warehouse = c_w_id + self.C_D_ID: int = c_d_id + self.C_ID: Optional[int] = None + self.H_AMOUNT: float = h_amount + self.H_DATE: str = h_date + + def __key__(self) -> str: + return self.txn_id + + + def get_customer_data(self, c_last: Optional[str]) -> Dict: + if self.C_ID is not None: + customer = get_entity_by_key( + Customer, (self.C_W_ID, self.C_D_ID, self.C_ID) + ) + return customer.pay(self.H_AMOUNT, self.D_ID, self.W_ID) + else: + customer_idx = get_entity_by_key( + CustomerIndex, (self.C_W_ID, self.C_D_ID, c_last) + ) + return customer_idx.pay(self.H_AMOUNT, self.D_ID, self.W_ID) + + def payment(self, params: dict) -> str: + w_id: Warehouse = params["W_ID"] + d_id: int = int(params["D_ID"]) + h_amount: float = params["H_AMOUNT"] + c_w_id: Warehouse = params["C_W_ID"] + c_d_id: int = int(params["C_D_ID"]) + + c_id: Optional[int] = int(params["C_ID"]) if params.get("C_ID") is not None else None + c_last: Optional[str] = params.get("C_LAST") + h_date: str = params["H_DATE"] + + self.W_ID = w_id + self.D_ID = d_id + self.C_ID = c_id + self.C_W_ID = c_w_id + self.C_D_ID = c_d_id + self.H_DATE = h_date + self.H_AMOUNT = h_amount + + district = get_entity_by_key(District, (w_id, d_id)) + + customer_data = self.get_customer_data(c_last) + district_data = district.pay(h_amount) + warehouse_data = w_id.pay(h_amount) + + # Build history record and persist it. + h_data = f"{warehouse_data['W_NAME']} {district_data['D_NAME']}" + send_async(History( + customer_data['C_ID'], self.C_D_ID, self.C_W_ID, + self.D_ID, self.W_ID, self.H_DATE, self.H_AMOUNT, h_data + )) + + if customer_data['C_CREDIT'] == "BC": + c_data_str = f",C_DATA={customer_data['C_DATA'][:200]}" + else: + c_data_str = "" + + return ( + f"P|W_ID={self.W_ID},D_ID={district_data['D_ID']},C_ID={customer_data['C_ID']}," + f"C_D_ID={customer_data['C_D_ID']},C_W_ID={customer_data['C_W_ID']}," + f"C_NAME={customer_data['C_FIRST']} {customer_data['C_MIDDLE']} {customer_data['C_LAST']}," + f"C_BAL={customer_data['C_BALANCE']:.2f},C_DISCOUNT={customer_data['C_DISCOUNT']:.4f}," + f"C_CREDIT={customer_data['C_CREDIT']},W_TAX={warehouse_data['W_TAX']:.4f}," + f"D_TAX={district_data['D_TAX']:.4f},H_AMOUNT={self.H_AMOUNT:.2f}," + f"H_DATE={self.H_DATE}{c_data_str}" + ) diff --git a/obol/examples/benchmarks/original/ycsb.py b/obol/examples/benchmarks/original/ycsb.py new file mode 100644 index 00000000..ec0cad64 --- /dev/null +++ b/obol/examples/benchmarks/original/ycsb.py @@ -0,0 +1,39 @@ +from obol.core import entity, send_async + +class NotEnoughCredit(Exception): + pass + +@entity +class YCSB: + def __init__(self, key: str): + self.key: str = key + self.value: int = 1_000_000 + + def __key__(self): + return self.key + + def get_key(self) -> str: + return self.key + + def get_value(self) -> int: return self.value + + def set_value(self, value: int): + self.value = value + + def read(self) -> tuple[YCSB, int]: + return self, self.value + + def update(self) -> tuple[YCSB, int]: + self.value += 1 + return self, self.value + + + def transfer(self, key_b: YCSB) -> tuple[YCSB, int]: + + send_async(key_b.update()) + + self.value -= 1 + if self.value < 0: + raise NotEnoughCredit(f"Not enough credit for user: {self}") + + return self, self.value diff --git a/obol/examples/benchmarks/plots_tpcc.py b/obol/examples/benchmarks/plots_tpcc.py new file mode 100644 index 00000000..d7afed93 --- /dev/null +++ b/obol/examples/benchmarks/plots_tpcc.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +import argparse +import glob +import json +import os +import re + +import matplotlib.pyplot as plt +import seaborn as sns + +HERE = os.path.dirname(__file__) +RESULTS = os.path.join(HERE, "results") +OUT = os.path.join(HERE, "figures") +os.makedirs(OUT, exist_ok=True) + + +def result_dirs(): + """Return every directory named ``results`` or ``results_run`` etc.""" + dirs = sorted( + d for d in glob.glob(os.path.join(HERE, "results*")) + if os.path.isdir(d) + ) + return dirs + +sns.set_theme( + context="paper", + style="whitegrid", + font_scale=1.05, + rc={ + "figure.dpi": 120, + "savefig.bbox": "tight", + "savefig.pad_inches": 0.04, + "axes.edgecolor": "0.25", + "grid.linewidth": 0.6, + "grid.linestyle": "--", + "lines.linewidth": 1.9, + "lines.markersize": 6, + }, +) + +# Maps the system token used in result filenames -> display name + style. +SYS = { + "obol_gather": dict(label="Obol (gather)", color="#5a2a82", marker="o"), + "obol_nogather": dict(label="Obol (no gather)", color="#b58fd6", marker="^"), + "handwritten": dict(label="Hand-written Styx", color="#c1492f", marker="s"), +} +# Order in which series are drawn / legended (bottom curve first). +DRAW_ORDER = ["handwritten", "obol_nogather", "obol_gather"] + +# tpcc__W__ALL.json +FNAME_RE = re.compile(r"^tpcc_(?P.+)_W(?P\d+)_(?P\d+)_ALL\.json$") + + +def load_results(dirs): + """Scan ``dirs`` and return {warehouses: {system: [(tput, p50, p99), ...]}}. + + Each ``(warehouses, system, tput)`` point is averaged over every directory + in ``dirs`` that contains a matching file. Only files that actually exist + contribute, so partial result sets just yield partial curves. + """ + # (wh, sys, tput) -> [(p50, p99), ...] collected across the given dirs. + samples = {} + for d in dirs: + for path in glob.glob(os.path.join(d, "tpcc_*_ALL.json")): + m = FNAME_RE.match(os.path.basename(path)) + if not m: + continue + sysname = m.group("sys") + if sysname not in SYS: + continue + wh = int(m.group("wh")) + tput = int(m.group("tput")) + + try: + with open(path) as f: + blob = json.load(f) + lat = blob["latency (ms)"] + p50 = float(lat["50"]) + p99 = float(lat["99"]) + except (json.JSONDecodeError, KeyError, ValueError, OSError): + continue + + samples.setdefault((wh, sysname, tput), []).append((p50, p99)) + + # Average the runs for each point, then group into per-warehouse curves. + data = {} + for (wh, sysname, tput), runs in samples.items(): + p50 = sum(r[0] for r in runs) / len(runs) + p99 = sum(r[1] for r in runs) / len(runs) + data.setdefault(wh, {}).setdefault(sysname, []).append((tput, p50, p99)) + + # Sort each curve by the (offered) input throughput. + for wh in data: + for sysname in data[wh]: + data[wh][sysname].sort(key=lambda r: r[0]) + return data + + +def draw_panel(ax, panel, title): + drawn_any = False + for sysname in DRAW_ORDER: + rows = panel.get(sysname) + if not rows: + continue + drawn_any = True + st = SYS[sysname] + tput = [r[0] for r in rows] + p50 = [r[1] for r in rows] + p99 = [r[2] for r in rows] + ax.plot(tput, p50, color=st["color"], marker=st["marker"], linestyle="-") + ax.plot(tput, p99, color=st["color"], marker=st["marker"], linestyle="--", + markerfacecolor="white", markersize=4.5, linewidth=1.3) + + ax.set_yscale("log") + ax.set_xlim(left=0) + ax.set_xlabel("Input throughput (txn/s)", labelpad=8) + ax.set_title(title) + if not drawn_any: + ax.text(0.5, 0.5, "no data yet", transform=ax.transAxes, + ha="center", va="center", color="0.5", fontsize=10) + sns.despine(ax=ax) + + +def make_figure(data, dpi, out_path): + warehouses = sorted(data) + n = len(warehouses) + + fig, axes = plt.subplots(1, n, figsize=(4.2 * n, 3.3), sharey=True, + squeeze=False) + axes = axes[0] + + letters = "abcdefghijklmnopqrstuvwxyz" + for ax, wh in zip(axes, warehouses): + draw_panel(ax, data[wh], f"({letters[warehouses.index(wh)]}) {wh} warehouses") + axes[0].set_ylabel("Latency (ms)") + + from matplotlib.lines import Line2D + # System legend: only show systems that appear in at least one panel. + present = [s for s in DRAW_ORDER + if any(s in data[wh] for wh in warehouses)] + handles = [ + Line2D([0], [0], color=SYS[s]["color"], marker=SYS[s]["marker"], + linestyle="-", label=SYS[s]["label"]) + for s in present + ] + style_handles = [ + Line2D([0], [0], color="0.3", linestyle="-", marker="o", + markersize=4.5, label=r"$p_{50}$"), + Line2D([0], [0], color="0.3", linestyle="--", marker="o", + markerfacecolor="white", markersize=4.5, label=r"$p_{99}$"), + ] + leg1 = fig.legend(handles=handles, ncol=len(handles) or 1, frameon=False, + fontsize=8, loc="upper center", bbox_to_anchor=(0.5, 1.08), + columnspacing=1.4, handletextpad=0.5) + fig.add_artist(leg1) + fig.legend(handles=style_handles, ncol=2, frameon=False, fontsize=8, + loc="upper center", bbox_to_anchor=(0.5, 1.005), + columnspacing=1.4, handletextpad=0.5) + + fig.tight_layout() + fig.savefig(out_path, dpi=dpi) + plt.close(fig) + print("wrote", out_path) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--hi-res", action="store_true") + dpi = 300 if ap.parse_args().hi_res else 200 + + dirs = result_dirs() + if not dirs: + print("no results* directories found in", HERE) + return + + # Figure 1: averaged across every results* run folder. + avg_data = load_results(dirs) + if avg_data: + make_figure(avg_data, dpi, os.path.join(OUT, "saturation_wh_avg.png")) + else: + print("no results found across", ", ".join(os.path.basename(d) for d in dirs)) + + # Figure 2: just the current `results` folder. + if os.path.isdir(RESULTS): + cur_data = load_results([RESULTS]) + if cur_data: + make_figure(cur_data, dpi, os.path.join(OUT, "saturation_wh.png")) + else: + print("no results found in", RESULTS) + + +if __name__ == "__main__": + main() diff --git a/obol/examples/benchmarks/plots_ycsb.py b/obol/examples/benchmarks/plots_ycsb.py new file mode 100644 index 00000000..c722ad3e --- /dev/null +++ b/obol/examples/benchmarks/plots_ycsb.py @@ -0,0 +1,204 @@ +import argparse +import glob +import json +import os +import re + +import matplotlib.pyplot as plt +import seaborn as sns + +HERE = os.path.dirname(__file__) +RESULTS = os.path.join(HERE, "results") +OUT = os.path.join(HERE, "figures") +os.makedirs(OUT, exist_ok=True) + + +def result_dirs(): + """Return every directory named ``results`` or ``results_run`` etc.""" + dirs = sorted( + d for d in glob.glob(os.path.join(HERE, "results*")) + if os.path.isdir(d) + ) + return dirs + + +sns.set_theme( + context="paper", + style="whitegrid", + font_scale=1.05, + rc={ + "figure.dpi": 120, + "savefig.bbox": "tight", + "savefig.pad_inches": 0.04, + "axes.edgecolor": "0.25", + "grid.linewidth": 0.6, + "grid.linestyle": "--", + "lines.linewidth": 1.9, + "lines.markersize": 6, + }, +) + +# Maps the system token used in result filenames -> display name + style. +SYS = { + "obol": dict(label="Obol", color="#5a2a82", marker="o"), + "handwritten": dict(label="Hand-written Styx", color="#c1492f", marker="s"), +} +# Order in which series are drawn / legended (bottom curve first). +DRAW_ORDER = ["handwritten", "obol"] + +# ycsbt__K_.json (uniform sweep, no zipf token) +FNAME_RE = re.compile(r"^ycsbt_(?P.+)_K(?P\d+)_(?P\d+)\.json$") + + +def load_results(dirs): + """Scan ``dirs`` and return {keys: {system: [(tput, p50, p99), ...]}}. + + Each ``(keys, system, tput)`` point is averaged over every directory in + ``dirs`` that contains a matching file, so partial result sets just yield + partial curves. + """ + # (keys, sys, tput) -> [(p50, p99), ...] collected across the given dirs. + samples = {} + for d in dirs: + for path in glob.glob(os.path.join(d, "ycsbt_*_K*_*.json")): + m = FNAME_RE.match(os.path.basename(path)) + if not m: + continue + sysname = m.group("sys") + if sysname not in SYS: + continue + keys = int(m.group("keys")) + tput = int(m.group("tput")) + + try: + with open(path) as f: + blob = json.load(f) + lat = blob["latency (ms)"] + p50 = float(lat["50"]) + p99 = float(lat["99"]) + except (json.JSONDecodeError, KeyError, ValueError, OSError): + continue + + samples.setdefault((keys, sysname, tput), []).append((p50, p99)) + + # Average the runs for each point, then group into per-key-space curves. + data = {} + for (keys, sysname, tput), runs in samples.items(): + p50 = sum(r[0] for r in runs) / len(runs) + p99 = sum(r[1] for r in runs) / len(runs) + data.setdefault(keys, {}).setdefault(sysname, []).append((tput, p50, p99)) + + # Sort each curve by the (offered) input throughput. + for keys in data: + for sysname in data[keys]: + data[keys][sysname].sort(key=lambda r: r[0]) + return data + + +def draw_panel(ax, panel, title): + drawn_any = False + for sysname in DRAW_ORDER: + rows = panel.get(sysname) + if not rows: + continue + drawn_any = True + st = SYS[sysname] + tput = [r[0] for r in rows] + p50 = [r[1] for r in rows] + p99 = [r[2] for r in rows] + ax.plot(tput, p50, color=st["color"], marker=st["marker"], linestyle="-") + ax.plot(tput, p99, color=st["color"], marker=st["marker"], linestyle="--", + markerfacecolor="white", markersize=4.5, linewidth=1.3) + + ax.set_yscale("log") + # YCSB spans ~100 .. ~50k txn/s (2-3 decades), so a log throughput axis + # keeps the low-rate flat region readable instead of crushing it at x=0. + ax.set_xscale("log") + ax.set_xlabel("Input throughput (txn/s)", labelpad=8) + ax.set_title(title) + if not drawn_any: + ax.text(0.5, 0.5, "no data yet", transform=ax.transAxes, + ha="center", va="center", color="0.5", fontsize=10) + sns.despine(ax=ax) + + +def _panel_title(letter, keys, n_panels): + """Label panels; for the standard 2-keyspace sweep, name the regime.""" + if n_panels == 2: + return f"({letter}) {keys:,} keys" + return f"({letter}) {keys:,} keys" + + +def make_figure(data, dpi, out_path): + global _panel_keys + _panel_keys = sorted(data) + keyspaces = _panel_keys + n = len(keyspaces) + + fig, axes = plt.subplots(1, n, figsize=(4.2 * n, 3.3), sharey=True, + squeeze=False) + axes = axes[0] + + letters = "abcdefghijklmnopqrstuvwxyz" + for ax, keys in zip(axes, keyspaces): + draw_panel(ax, data[keys], _panel_title(letters[keyspaces.index(keys)], keys, n)) + axes[0].set_ylabel("Latency (ms)") + + from matplotlib.lines import Line2D + # System legend: only show systems that appear in at least one panel. + present = [s for s in DRAW_ORDER + if any(s in data[keys] for keys in keyspaces)] + handles = [ + Line2D([0], [0], color=SYS[s]["color"], marker=SYS[s]["marker"], + linestyle="-", label=SYS[s]["label"]) + for s in present + ] + style_handles = [ + Line2D([0], [0], color="0.3", linestyle="-", marker="o", + markersize=4.5, label=r"$p_{50}$"), + Line2D([0], [0], color="0.3", linestyle="--", marker="o", + markerfacecolor="white", markersize=4.5, label=r"$p_{99}$"), + ] + leg1 = fig.legend(handles=handles, ncol=len(handles) or 1, frameon=False, + fontsize=8, loc="upper center", bbox_to_anchor=(0.5, 1.08), + columnspacing=1.4, handletextpad=0.5) + fig.add_artist(leg1) + fig.legend(handles=style_handles, ncol=2, frameon=False, fontsize=8, + loc="upper center", bbox_to_anchor=(0.5, 1.005), + columnspacing=1.4, handletextpad=0.5) + + fig.tight_layout() + fig.savefig(out_path, dpi=dpi) + plt.close(fig) + print("wrote", out_path) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--hi-res", action="store_true") + dpi = 300 if ap.parse_args().hi_res else 200 + + dirs = result_dirs() + if not dirs: + print("no results* directories found in", HERE) + return + + # Figure 1: averaged across every results* run folder. + avg_data = load_results(dirs) + if avg_data: + make_figure(avg_data, dpi, os.path.join(OUT, "ycsb_results_avg.png")) + else: + print("no YCSB results found across", + ", ".join(os.path.basename(d) for d in dirs)) + + # Figure 2: just the current `results` folder. + if os.path.isdir(RESULTS): + cur_data = load_results([RESULTS]) + if cur_data: + make_figure(cur_data, dpi, os.path.join(OUT, "ycsb_results.png")) + else: + print("no YCSB results found in", RESULTS) + + +if __name__ == "__main__": + main() diff --git a/obol/examples/compiled/marketplace.py b/obol/examples/compiled/marketplace.py new file mode 100644 index 00000000..fd8b726f --- /dev/null +++ b/obol/examples/compiled/marketplace.py @@ -0,0 +1,3477 @@ +from styx.common.operator import Operator +from styx.common.stateful_function import StatefulFunction +from styx.common.logging import logging + +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + if barrier["total"] == 0: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + return True, (), barrier["saved"], barrier["parent_reply_to"] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None + +from typing import Optional + + +# ────────────────────────────────────────── +# Custom Exceptions (all trigger rollback) +# ────────────────────────────────────────── + +class InsufficientFunds(Exception): + pass + +class InsufficientStock(Exception): + pass + +class InvalidCoupon(Exception): + pass + +class SellerSuspended(Exception): + pass + +class OrderAlreadyFulfilled(Exception): + pass + +class WarehouseCapacityExceeded(Exception): + pass + +class ReviewAlreadySubmitted(Exception): + pass +product_operator = Operator('product', n_partitions=4) + +@product_operator.register +async def insert(ctx: StatefulFunction, product_id: str, name: str, base_price: int, seller: 'Seller', reply_to: list = None): + __state__ = {} + __state__['product_id'] = product_id + __state__['name'] = name + __state__['base_price'] = base_price + __state__['seller'] = seller + __state__['stock'] = 0 + __state__['total_sold'] = 0 + __state__['rating_sum'] = 0 + __state__['rating_count'] = 0 + __state__['tags'] = [] + __state__['is_active'] = True + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@product_operator.register +async def get_product_id(ctx: StatefulFunction, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['product_id']) + + +@product_operator.register +async def get_price(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['base_price']) + + +@product_operator.register +async def get_stock(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['stock']) + + +@product_operator.register +async def get_seller(ctx: StatefulFunction, reply_to: list = None) -> 'Seller': + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['seller']) + + +@product_operator.register +async def is_available(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['is_active'] & (__state__['stock'] > 0)) + + +@product_operator.register +async def get_average_rating(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + if __state__['rating_count'] == 0: + ctx.put(__state__) + return send_reply(ctx, reply_to, 0) + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['rating_sum'] // __state__['rating_count']) + + +@product_operator.register +async def add_stock(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if amount <= 0: + raise InsufficientStock("Stock amount must be positive.") + __state__['stock'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@product_operator.register +async def deduct_stock(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if amount <= 0: + raise InsufficientStock("Amount must be positive.") + if not __state__['is_active']: + raise InsufficientStock("Product is no longer active.") + if __state__['stock'] < amount: + raise InsufficientStock("Not enough stock for product.") + __state__['stock'] -= amount + __state__['total_sold'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + + +@product_operator.register +async def add_rating(ctx: StatefulFunction, score: int, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + if (score < 0) | (score > 10): + raise ValueError("Rating must be between 0 and 10.") + __state__['rating_sum'] += score + __state__['rating_count'] += 1 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_average_rating', key = ctx.key, params = (reply_to,)) + + +@product_operator.register +async def deactivate(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['is_active'] = False + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@product_operator.register +async def add_tag(ctx: StatefulFunction, tag: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if tag not in __state__['tags']: + __state__['tags'].append(tag) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@product_operator.register +async def get_tags(ctx: StatefulFunction, reply_to: list = None) -> list[str]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['tags']) + + +@product_operator.register +async def get_total_sold(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['total_sold']) + + +@product_operator.register +async def get_popularity_score(ctx: StatefulFunction, reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'product', 'get_popularity_score_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_average_rating', key = ctx.key, params = (reply_to,)) + +@product_operator.register +async def get_popularity_score_step_2(ctx: StatefulFunction, func_context, avg = None, reply_to: list = None): + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['total_sold'] * 10 + avg * 50 + __state__['rating_count'] * 5) + +seller_operator = Operator('seller', n_partitions=4) + +@seller_operator.register +async def insert(ctx: StatefulFunction, seller_id: str, name: str, reply_to: list = None): + __state__ = {} + __state__['seller_id'] = seller_id + __state__['name'] = name + __state__['balance'] = 0 + __state__['products'] = [] + __state__['total_revenue'] = 0 + __state__['is_suspended'] = False + __state__['penalty_points'] = 0 + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@seller_operator.register +async def get_seller_id(ctx: StatefulFunction, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['seller_id']) + + +@seller_operator.register +async def is_active(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, not __state__['is_suspended']) + + +@seller_operator.register +async def get_balance(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['balance']) + + +@seller_operator.register +async def get_revenue(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['total_revenue']) + + +@seller_operator.register +async def add_product(ctx: StatefulFunction, product: 'Product', reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if __state__['is_suspended']: + raise SellerSuspended("Seller is suspended and cannot add products.") + __state__['products'].append(product) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@seller_operator.register +async def credit_sale(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if __state__['is_suspended']: + raise SellerSuspended("Seller is suspended.") + __state__['balance'] += amount + __state__['total_revenue'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@seller_operator.register +async def debit_penalty(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['penalty_points'] += 1 + __state__['balance'] -= amount + if __state__['penalty_points'] >= 5: + __state__['is_suspended'] = True + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@seller_operator.register +async def withdraw(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if __state__['balance'] < amount: + raise InsufficientFunds("Seller does not have enough balance to withdraw.") + __state__['balance'] -= amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@seller_operator.register +async def get_products(ctx: StatefulFunction, reply_to: list = None) -> list['Product']: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['products']) + + +@seller_operator.register +async def get_penalty_points(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['penalty_points']) + + +@seller_operator.register +async def reinstate(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['is_suspended'] = False + __state__['penalty_points'] = 0 + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + +customer_operator = Operator('customer', n_partitions=4) + +@customer_operator.register +async def insert(ctx: StatefulFunction, customer_id: str, username: str, reply_to: list = None): + __state__ = {} + __state__['customer_id'] = customer_id + __state__['username'] = username + __state__['balance'] = 0 + __state__['cart'] = [] + __state__['order_history'] = [] + __state__['wishlist'] = [] + __state__['loyalty_points'] = 0 + __state__['reviewed_products'] = [] + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@customer_operator.register +async def get_order_history(ctx: StatefulFunction, reply_to: list = None) -> list[str]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['order_history']) + + +@customer_operator.register +async def get_balance(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['balance']) + + +@customer_operator.register +async def get_loyalty_points(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['loyalty_points']) + + +@customer_operator.register +async def add_funds(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['balance'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def deduct_funds(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if __state__['balance'] < amount: + raise InsufficientFunds("Customer does not have enough balance.") + __state__['balance'] -= amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def add_to_cart(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if product_id not in __state__['cart']: + __state__['cart'].append(product_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def remove_from_cart(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if product_id in __state__['cart']: + __state__['cart'].remove(product_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def clear_cart(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['cart'] = [] + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def get_cart(ctx: StatefulFunction, reply_to: list = None) -> list[str]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['cart']) + + +@customer_operator.register +async def add_to_wishlist(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if product_id not in __state__['wishlist']: + __state__['wishlist'].append(product_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def add_order(ctx: StatefulFunction, order_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['order_history'].append(order_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def earn_loyalty_points(ctx: StatefulFunction, amount_spent: int, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + earned = amount_spent // 100 + __state__['loyalty_points'] += earned + ctx.put(__state__) + return send_reply(ctx, reply_to, earned) + + +@customer_operator.register +async def redeem_loyalty_points(ctx: StatefulFunction, points: int, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + if __state__['loyalty_points'] < points: + points = __state__['loyalty_points'] + __state__['loyalty_points'] -= points + ctx.put(__state__) + return send_reply(ctx, reply_to, points * 10) + + +@customer_operator.register +async def has_reviewed(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, product_id in __state__['reviewed_products']) + + +@customer_operator.register +async def mark_reviewed(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if product_id in __state__['reviewed_products']: + raise ReviewAlreadySubmitted("Customer already reviewed this product.") + __state__['reviewed_products'].append(product_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@customer_operator.register +async def get_order_count(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, len(__state__['order_history'])) + + +@customer_operator.register +async def get_wishlist(ctx: StatefulFunction, reply_to: list = None) -> list[str]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['wishlist']) + +coupon_operator = Operator('coupon', n_partitions=4) + +@coupon_operator.register +async def insert(ctx: StatefulFunction, code: str, discount_percent: int, max_uses: int, min_order_value: int, reply_to: list = None): + __state__ = {} + __state__['code'] = code + __state__['discount_percent'] = discount_percent + __state__['max_uses'] = max_uses + __state__['uses'] = 0 + __state__['min_order_value'] = min_order_value + __state__['is_active'] = True + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@coupon_operator.register +async def is_valid(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['is_active'] & (__state__['uses'] < __state__['max_uses'])) + + +@coupon_operator.register +async def get_discount_percent(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['discount_percent']) + + +@coupon_operator.register +async def get_min_order_value(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['min_order_value']) + + +@coupon_operator.register +async def apply(ctx: StatefulFunction, order_value: int, reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'coupon', 'apply_step_2', ctx.key, {'order_value': order_value}) + ctx.call_remote_async(operator_name = 'coupon', function_name = 'is_valid', key = ctx.key, params = (reply_to,)) + +@coupon_operator.register +async def apply_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (order_value,) = (params.get('order_value'),) + if not attr_1: + raise InvalidCoupon("Coupon is expired or has reached max uses.") + if order_value < __state__['min_order_value']: + raise InvalidCoupon("Order value too low for this coupon.") + __state__['uses'] += 1 + discount = (order_value * __state__['discount_percent']) // 100 + ctx.put(__state__) + return send_reply(ctx, reply_to, discount) + + +@coupon_operator.register +async def deactivate(ctx: StatefulFunction, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['is_active'] = False + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@coupon_operator.register +async def get_remaining_uses(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['max_uses'] - __state__['uses']) + +warehouse_operator = Operator('warehouse', n_partitions=4) + +@warehouse_operator.register +async def insert(ctx: StatefulFunction, warehouse_id: str, capacity: int, reply_to: list = None): + __state__ = {} + __state__['warehouse_id'] = warehouse_id + __state__['capacity'] = capacity + __state__['used_capacity'] = 0 + __state__['product_slots'] = {} + __state__['pending_shipments'] = [] + __state__['total_shipped'] = 0 + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@warehouse_operator.register +async def get_available_capacity(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['capacity'] - __state__['used_capacity']) + + +@warehouse_operator.register +async def get_used_capacity(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['used_capacity']) + + +@warehouse_operator.register +async def store_product(ctx: StatefulFunction, product_id: str, quantity: int, reply_to: list = None) -> bool: + reply_to = push_continuation(ctx, reply_to, 'warehouse', 'store_product_step_2', ctx.key, {'product_id': product_id, 'quantity': quantity}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_available_capacity', key = ctx.key, params = (reply_to,)) + +@warehouse_operator.register +async def store_product_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (product_id, quantity) = (params.get('product_id'), params.get('quantity')) + if quantity > attr_1: + raise WarehouseCapacityExceeded("Not enough space in warehouse.") + current = __state__['product_slots'].get(product_id, 0) + __state__['product_slots'][product_id] = current + quantity + __state__['used_capacity'] += quantity + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@warehouse_operator.register +async def remove_product(ctx: StatefulFunction, product_id: str, quantity: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if quantity <= 0: + raise InsufficientStock("Quantity must be positive.") + current = __state__['product_slots'].get(product_id, 0) + if current < quantity: + raise InsufficientStock("Not enough of this product in warehouse.") + new_qty = current - quantity + + if new_qty == 0: + del __state__['product_slots'][product_id] + else: + __state__['product_slots'][product_id] = new_qty + __state__['used_capacity'] -= quantity + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@warehouse_operator.register +async def get_product_quantity(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + attr_1 = __state__['product_slots'].get(product_id, 0) + ctx.put(__state__) + return send_reply(ctx, reply_to, attr_1) + + +@warehouse_operator.register +async def add_pending_shipment(ctx: StatefulFunction, order_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['pending_shipments'].append(order_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@warehouse_operator.register +async def dispatch_shipment(ctx: StatefulFunction, order_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if order_id not in __state__['pending_shipments']: + raise OrderAlreadyFulfilled("Order not found in pending shipments.") + __state__['pending_shipments'].remove(order_id) + __state__['total_shipped'] += 1 + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@warehouse_operator.register +async def get_total_shipped(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['total_shipped']) + + +@warehouse_operator.register +async def get_pending_count(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, len(__state__['pending_shipments'])) + + +@warehouse_operator.register +async def calculate_fill_rate(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + if __state__['capacity'] == 0: + ctx.put(__state__) + return send_reply(ctx, reply_to, 0) + ctx.put(__state__) + return send_reply(ctx, reply_to, (__state__['used_capacity'] * 100) // __state__['capacity']) + +marketplace_operator = Operator('marketplace', n_partitions=4) + +@marketplace_operator.register +async def insert(ctx: StatefulFunction, marketplace_id: str, reply_to: list = None): + __state__ = {} + __state__['marketplace_id'] = marketplace_id + __state__['registered_sellers'] = [] + __state__['registered_customers'] = [] + __state__['all_products'] = [] + __state__['total_transactions'] = 0 + __state__['total_revenue'] = 0 + __state__['platform_fee_percent'] = 5 + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@marketplace_operator.register +async def register_seller(ctx: StatefulFunction, seller_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if seller_id not in __state__['registered_sellers']: + __state__['registered_sellers'].append(seller_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@marketplace_operator.register +async def register_customer(ctx: StatefulFunction, customer_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if customer_id not in __state__['registered_customers']: + __state__['registered_customers'].append(customer_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@marketplace_operator.register +async def list_product(ctx: StatefulFunction, product_id: str, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if product_id not in __state__['all_products']: + __state__['all_products'].append(product_id) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@marketplace_operator.register +async def record_transaction(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + fee = (amount * __state__['platform_fee_percent']) // 100 + __state__['total_revenue'] += fee + __state__['total_transactions'] += 1 + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@marketplace_operator.register +async def get_stats(ctx: StatefulFunction, reply_to: list = None) -> dict[str, int]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, { + "sellers": len(__state__['registered_sellers']), + "customers": len(__state__['registered_customers']), + "products": len(__state__['all_products']), + "transactions": __state__['total_transactions'], + "revenue": __state__['total_revenue'], + }) + + +@marketplace_operator.register +async def get_platform_fee(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['platform_fee_percent']) + + +@marketplace_operator.register +async def get_total_revenue(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['total_revenue']) + + +@marketplace_operator.register +async def get_product_count(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, len(__state__['all_products'])) + + +# ── Complex orchestration methods ────── + +@marketplace_operator.register +async def purchase( + ctx: StatefulFunction, customer: str, + product: str, + warehouse: str, + quantity: int, + coupon_code: Optional[str], + coupon: Optional[str], + use_loyalty: bool, +reply_to: list = None) -> str: + + if quantity <= 0: + raise ValueError("Quantity must be positive.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_2', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_2(ctx: StatefulFunction, func_context, seller = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('use_loyalty'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_3', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_3(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, seller, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + + if not attr_2: + raise InsufficientStock("Product not available.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_4', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'is_active', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_4(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, seller, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + + if not attr_3: + raise SellerSuspended("Seller is suspended.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_5', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_5(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, seller, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_6', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_product_quantity', key = warehouse, params = (attr_4, reply_to)) + +@marketplace_operator.register +async def purchase_step_6(ctx: StatefulFunction, func_context, attr_5 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, seller, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + + if attr_5 < quantity: + raise InsufficientStock("Warehouse does not have enough stock.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_7', ctx.key, {'coupon': coupon, 'coupon_code': coupon_code, 'customer': customer, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_7(ctx: StatefulFunction, func_context, attr_6 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, coupon_code, customer, product, quantity, seller, use_loyalty, warehouse) = (params.get('coupon'), params.get('coupon_code'), params.get('customer'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + base_cost = attr_6 * quantity + discount = 0 + + if coupon is not None: + if coupon_code is not None: + if coupon.code != coupon_code: + raise InvalidCoupon("Coupon code mismatch.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_8', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'product': product, 'quantity': quantity, 'seller': seller, 'use_loyalty': use_loyalty, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'coupon', function_name = 'apply', key = coupon, params = (base_cost, reply_to)) + else: + loyalty_discount = 0 + if use_loyalty: + max_loyalty_discount = int(base_cost * 0.3) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_43', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'max_loyalty_discount': max_loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_loyalty_points', key = customer, params = (reply_to,)) + else: + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_61', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + else: + loyalty_discount = 0 + if use_loyalty: + max_loyalty_discount = int(base_cost * 0.3) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_77', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'max_loyalty_discount': max_loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_loyalty_points', key = customer, params = (reply_to,)) + else: + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_95', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_8(ctx: StatefulFunction, func_context, discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, product, quantity, seller, use_loyalty, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('use_loyalty'), params.get('warehouse')) + loyalty_discount = 0 + if use_loyalty: + max_loyalty_discount = int(base_cost * 0.3) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_9', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'max_loyalty_discount': max_loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_loyalty_points', key = customer, params = (reply_to,)) + else: + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_27', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_9(ctx: StatefulFunction, func_context, attr_8 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, max_loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('max_loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + redeemable_points = attr_8 // 2 + potential_discount = redeemable_points * 10 + actual_discount = min(max_loyalty_discount, potential_discount) + points_to_use = actual_discount // 10 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_10', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'loyalty_discount': loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'redeem_loyalty_points', key = customer, params = (points_to_use, reply_to)) + +@marketplace_operator.register +async def purchase_step_10(ctx: StatefulFunction, func_context, loyalty_discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_11', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_11(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_12', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_12(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_13', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_13(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_14', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_14(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_15', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_15(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_16', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_22', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_16(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_17', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_17(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_18', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_18(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_19', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_19(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_20', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_20(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_21', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_21(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_22(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_23', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_23(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_24', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_24(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_25', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_25(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_26', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_26(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_27(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_28', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_28(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_29', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_29(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_30', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_30(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_31', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_31(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_32', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_38', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_32(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_33', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_33(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_34', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_34(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_35', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_35(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_36', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_36(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_37', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_37(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_38(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_39', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_39(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_40', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_40(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_41', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_41(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_42', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_42(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_43(ctx: StatefulFunction, func_context, attr_8 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, max_loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('max_loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + redeemable_points = attr_8 // 2 + potential_discount = redeemable_points * 10 + actual_discount = min(max_loyalty_discount, potential_discount) + points_to_use = actual_discount // 10 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_44', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'loyalty_discount': loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'redeem_loyalty_points', key = customer, params = (points_to_use, reply_to)) + +@marketplace_operator.register +async def purchase_step_44(ctx: StatefulFunction, func_context, loyalty_discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_45', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_45(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_46', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_46(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_47', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_47(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_48', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_48(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_49', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_49(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_50', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_56', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_50(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_51', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_51(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_52', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_52(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_53', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_53(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_54', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_54(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_55', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_55(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_56(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_57', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_57(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_58', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_58(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_59', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_59(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_60', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_60(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_61(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_62', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_62(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_63', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_63(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_64', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_64(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_65', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_65(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_66', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_72', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_66(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_67', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_67(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_68', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_68(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_69', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_69(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_70', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_70(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_71', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_71(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_72(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_73', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_73(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_74', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_74(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_75', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_75(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_76', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_76(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_77(ctx: StatefulFunction, func_context, attr_8 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, max_loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('max_loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + redeemable_points = attr_8 // 2 + potential_discount = redeemable_points * 10 + actual_discount = min(max_loyalty_discount, potential_discount) + points_to_use = actual_discount // 10 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_78', ctx.key, {'base_cost': base_cost, 'customer': customer, 'discount': discount, 'loyalty_discount': loyalty_discount, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'redeem_loyalty_points', key = customer, params = (points_to_use, reply_to)) + +@marketplace_operator.register +async def purchase_step_78(ctx: StatefulFunction, func_context, loyalty_discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (base_cost, customer, discount, loyalty_discount, product, quantity, seller, warehouse) = (params.get('base_cost'), params.get('customer'), params.get('discount'), params.get('loyalty_discount'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_79', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_79(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_80', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_80(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_81', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_81(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_82', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_82(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_83', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_83(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_84', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_90', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_84(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_85', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_85(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_86', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_86(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_87', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_87(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_88', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_88(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_89', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_89(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_90(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_91', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_91(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_92', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_92(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_93', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_93(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_94', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_94(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_95(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_96', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_96(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_97', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'quantity': quantity, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_97(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, quantity, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('quantity'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_98', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_12, quantity, reply_to)) + +@marketplace_operator.register +async def purchase_step_98(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + platform_fee = (final_cost * __state__['platform_fee_percent']) // 100 + seller_cut = final_cost - platform_fee + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_99', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (seller_cut, reply_to)) + +@marketplace_operator.register +async def purchase_step_99(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + + if final_cost > 0: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_100', ctx.key, {'customer': customer, 'final_cost': final_cost, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final_cost, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_106', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_100(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final_cost, product, seller, warehouse) = (params.get('customer'), params.get('final_cost'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_101', ctx.key, {'customer': customer, 'product': product, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final_cost, reply_to)) + +@marketplace_operator.register +async def purchase_step_101(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_102', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_102(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_103', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_103(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_104', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_104(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_105', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_105(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + +@marketplace_operator.register +async def purchase_step_106(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, seller, warehouse) = (params.get('customer'), params.get('product'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_107', ctx.key, {'customer': customer, 'product': product, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_107(ctx: StatefulFunction, func_context, attr_17 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, warehouse) = (params.get('customer'), params.get('product'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_108', ctx.key, {'attr_17': attr_17, 'customer': customer, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def purchase_step_108(ctx: StatefulFunction, func_context, attr_18 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (attr_17, customer, warehouse) = (params.get('attr_17'), params.get('customer'), params.get('warehouse')) + order_id = ( + attr_17 + + "_" + + attr_18 + + "_" + + str(__state__['total_transactions']) + ) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_109', ctx.key, {'order_id': order_id, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'customer', function_name = 'add_order', key = customer, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_109(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id, warehouse) = (params.get('order_id'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'purchase_step_110', ctx.key, {'order_id': order_id}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'add_pending_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def purchase_step_110(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (order_id,) = (params.get('order_id'),) + return send_reply(ctx, reply_to, order_id) + + + +@marketplace_operator.register +async def batch_restock( + ctx: StatefulFunction, products: list[str], + quantities: list[int], + warehouse: str, +reply_to: list = None) -> str: + restocked = 0 + skipped = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'batch_restock_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def batch_restock_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'batch_restock_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + p = products[i] + qty = quantities[i] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'batch_restock_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_available_capacity', key = warehouse, params = (reply_to,)) + +@marketplace_operator.register +async def batch_restock_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + return send_reply(ctx, reply_to, "Restocked: " + str(restocked) + ", Skipped: " + str(skipped)) + +@marketplace_operator.register +async def batch_restock_step_4(ctx: StatefulFunction, func_context, available_space = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, p, products, qty, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + + if available_space >= qty: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'batch_restock_step_5', ctx.key, {'__loop_index_1': __loop_index_1, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'add_stock', key = p, params = (qty, reply_to)) + else: + skipped += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'batch_restock_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def batch_restock_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, p, products, qty, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'batch_restock_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'products': products, 'qty': qty, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def batch_restock_step_6(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, qty, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'batch_restock_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'store_product', key = warehouse, params = (attr_3, qty, reply_to)) + +@marketplace_operator.register +async def batch_restock_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, restocked, skipped, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('restocked'), params.get('skipped'), params.get('warehouse')) + restocked += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'batch_restock_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'restocked': restocked, 'skipped': skipped, 'warehouse': warehouse}, None, reply_to)) + + +@marketplace_operator.register +async def compute_cart_total( + ctx: StatefulFunction, customer: str, + products: list[str], + quantities: list[int], +reply_to: list = None) -> int: + total = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_cart_total_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'total': total}, None, reply_to)) + +@marketplace_operator.register +async def compute_cart_total_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, total) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('total')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_cart_total_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'total': total}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + p = products[i] + qty = quantities[i] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'compute_cart_total_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'products': products, 'qty': qty, 'quantities': quantities, 'total': total}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def compute_cart_total_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, total) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('total')) + return send_reply(ctx, reply_to, total) + +@marketplace_operator.register +async def compute_cart_total_step_4(ctx: StatefulFunction, func_context, price = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, qty, quantities, total) = (params.get('__loop_index_1'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('total')) + + if qty <= 5: + item_total = qty * price + elif qty <= 20: + item_total = 5 * price + int((qty - 5) * price * 0.9) + else: + item_total = ( + 5 * price + + int(15 * price * 0.9) + + int((qty - 20) * price * 0.8) + ) + total += item_total + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_cart_total_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'total': total}, None, reply_to)) + + +@marketplace_operator.register +async def submit_review( + ctx: StatefulFunction, customer: str, + product: str, + score: int, +reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'submit_review_step_2', ctx.key, {'customer': customer, 'product': product, 'score': score}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def submit_review_step_2(ctx: StatefulFunction, func_context, product_id = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, product, score) = (params.get('customer'), params.get('product'), params.get('score')) + _comp_result_1 = [] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'submit_review_step_3', ctx.key, {'_comp_result_1': _comp_result_1, 'customer': customer, 'product': product, 'product_id': product_id, 'score': score}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_order_history', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def submit_review_step_3(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (_comp_result_1, customer, product, product_id, score) = (params.get('_comp_result_1'), params.get('customer'), params.get('product'), params.get('product_id'), params.get('score')) + for order_id in attr_3: + _comp_result_1.append(product_id in order_id) + has_purchased = any(_comp_result_1) + if not has_purchased: + raise Exception("Customer cannot review a product they haven't purchased.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'submit_review_step_4', ctx.key, {'_comp_result_1': _comp_result_1, 'customer': customer, 'product': product, 'score': score}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def submit_review_step_4(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (_comp_result_1, customer, product, score) = (params.get('_comp_result_1'), params.get('customer'), params.get('product'), params.get('score')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'submit_review_step_5', ctx.key, {'_comp_result_1': _comp_result_1, 'customer': customer, 'product': product, 'score': score}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'mark_reviewed', key = customer, params = (attr_4, reply_to)) + +@marketplace_operator.register +async def submit_review_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (_comp_result_1, customer, product, score) = (params.get('_comp_result_1'), params.get('customer'), params.get('product'), params.get('score')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'submit_review_step_6', ctx.key, {'_comp_result_1': _comp_result_1, 'customer': customer}) + ctx.call_remote_async(operator_name = 'product', function_name = 'add_rating', key = product, params = (score, reply_to)) + +@marketplace_operator.register +async def submit_review_step_6(ctx: StatefulFunction, func_context, new_avg = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (_comp_result_1, customer) = (params.get('_comp_result_1'), params.get('customer')) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (20, [{'sink': True}])) + return send_reply(ctx, reply_to, new_avg) + + +@marketplace_operator.register +async def get_top_product_scores(ctx: StatefulFunction, products: list[str], reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_top_product_scores_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def get_top_product_scores_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_top_product_scores_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_top_product_scores_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_popularity_score', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_top_product_scores_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + scores = _comp_result_1 + return send_reply(ctx, reply_to, scores) + +@marketplace_operator.register +async def get_top_product_scores_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_top_product_scores_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def get_affordable_products( + ctx: StatefulFunction, products: list[str], budget: int, +reply_to: list = None) -> list[str]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_affordable_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'budget': budget, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def get_affordable_products_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, budget, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('budget'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_affordable_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'budget': budget, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_affordable_products_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'budget': budget, 'p': p, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_affordable_products_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, budget, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('budget'), params.get('products')) + affordable = _comp_result_1 + return send_reply(ctx, reply_to, affordable) + +@marketplace_operator.register +async def get_affordable_products_step_4(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, budget, p, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('budget'), params.get('p'), params.get('products')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_affordable_products_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'attr_2': attr_2, 'budget': budget, 'p': p, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_affordable_products_step_5(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, attr_2, budget, p, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('attr_2'), params.get('budget'), params.get('p'), params.get('products')) + if (attr_2 <= budget) & attr_3: + _comp_result_1.append(p) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_affordable_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'budget': budget, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def total_wishlist_value(ctx: StatefulFunction, customer: str, products: list[str], reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'total_wishlist_value_step_2', ctx.key, {'products': products}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_wishlist', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def total_wishlist_value_step_2(ctx: StatefulFunction, func_context, wishlist = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products,) = (params.get('products'),) + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_wishlist_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'wishlist': wishlist}, None, reply_to)) + +@marketplace_operator.register +async def total_wishlist_value_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, wishlist) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('wishlist')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_wishlist_value_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'wishlist': wishlist}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'total_wishlist_value_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'p': p, 'products': products, 'wishlist': wishlist}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def total_wishlist_value_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, wishlist) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('wishlist')) + total = sum(_comp_result_1) + return send_reply(ctx, reply_to, total) + +@marketplace_operator.register +async def total_wishlist_value_step_5(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, p, products, wishlist) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('p'), params.get('products'), params.get('wishlist')) + if attr_4 in wishlist: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'total_wishlist_value_step_6', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'wishlist': wishlist}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_wishlist_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'wishlist': wishlist}, None, reply_to)) + +@marketplace_operator.register +async def total_wishlist_value_step_6(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, wishlist) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('wishlist')) + _comp_result_1.append(attr_2) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_wishlist_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'wishlist': wishlist}, None, reply_to)) + + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products( + ctx: StatefulFunction, seller: str, + products: list[str], +reply_to: list = None) -> str: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'suspend_seller_and_deactivate_products_step_2', ctx.key, {'products': products, 'seller': seller}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'debit_penalty', key = seller, params = (500, reply_to)) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products, seller) = (params.get('products'), params.get('seller')) + deactivated = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'suspend_seller_and_deactivate_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'products': products, 'seller': seller}, None, reply_to)) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, deactivated, products, seller) = (params.get('__loop_index_1'), params.get('deactivated'), params.get('products'), params.get('seller')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'suspend_seller_and_deactivate_products_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'products': products, 'seller': seller}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'suspend_seller_and_deactivate_products_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'p': p, 'products': products, 'seller': seller}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, deactivated, products, seller) = (params.get('__loop_index_1'), params.get('deactivated'), params.get('products'), params.get('seller')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'suspend_seller_and_deactivate_products_step_5', ctx.key, {'__loop_index_1': __loop_index_1, 'deactivated': deactivated}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_5(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, deactivated) = (params.get('__loop_index_1'), params.get('deactivated')) + return send_reply(ctx, reply_to, "Deactivated " + str(deactivated) + " products for seller " + attr_4) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_6(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, deactivated, p, products, seller) = (params.get('__loop_index_1'), params.get('deactivated'), params.get('p'), params.get('products'), params.get('seller')) + if (attr_3 == seller): + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'suspend_seller_and_deactivate_products_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'products': products, 'seller': seller}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deactivate', key = p, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'suspend_seller_and_deactivate_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'products': products, 'seller': seller}, None, reply_to)) + +@marketplace_operator.register +async def suspend_seller_and_deactivate_products_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, deactivated, products, seller) = (params.get('__loop_index_1'), params.get('deactivated'), params.get('products'), params.get('seller')) + deactivated += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'suspend_seller_and_deactivate_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'deactivated': deactivated, 'products': products, 'seller': seller}, None, reply_to)) + + +@marketplace_operator.register +async def restock_and_report( + ctx: StatefulFunction, seller: str, + products: list[str], + quantities: list[int], + warehouse: str, +reply_to: list = None) -> dict[str, int]: + total_units = 0 + total_fee = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'restock_and_report_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def restock_and_report_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'restock_and_report_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + p = products[i] + qty = quantities[i] + fee = qty * 2 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'restock_and_report_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'fee': fee, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_available_capacity', key = warehouse, params = (reply_to,)) + +@marketplace_operator.register +async def restock_and_report_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + return send_reply(ctx, reply_to, {"units_restocked": total_units, "fees_charged": total_fee}) + +@marketplace_operator.register +async def restock_and_report_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, fee, p, products, qty, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('fee'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + + if attr_1 < qty: + raise WarehouseCapacityExceeded("Cannot restock: warehouse is full.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'restock_and_report_step_5', ctx.key, {'__loop_index_1': __loop_index_1, 'fee': fee, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'withdraw', key = seller, params = (fee, reply_to)) + +@marketplace_operator.register +async def restock_and_report_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, fee, p, products, qty, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('fee'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'restock_and_report_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'fee': fee, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'add_stock', key = p, params = (qty, reply_to)) + +@marketplace_operator.register +async def restock_and_report_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, fee, p, products, qty, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('fee'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'restock_and_report_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'fee': fee, 'products': products, 'qty': qty, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def restock_and_report_step_7(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, fee, products, qty, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('fee'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'restock_and_report_step_8', ctx.key, {'__loop_index_1': __loop_index_1, 'fee': fee, 'products': products, 'qty': qty, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'store_product', key = warehouse, params = (attr_4, qty, reply_to)) + +@marketplace_operator.register +async def restock_and_report_step_8(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, fee, products, qty, quantities, seller, total_fee, total_units, warehouse) = (params.get('__loop_index_1'), params.get('fee'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('seller'), params.get('total_fee'), params.get('total_units'), params.get('warehouse')) + total_units += qty + total_fee += fee + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'restock_and_report_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'total_fee': total_fee, 'total_units': total_units, 'warehouse': warehouse}, None, reply_to)) + + +@marketplace_operator.register +async def process_bulk_orders( + ctx: StatefulFunction, customers: list[str], + product: str, + quantity_each: int, + warehouse: str, +reply_to: list = None) -> str: + + if quantity_each <= 0: + raise ValueError("Quantity must be positive.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_2', ctx.key, {'customers': customers, 'product': product, 'quantity_each': quantity_each, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_2(ctx: StatefulFunction, func_context, seller = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customers, product, quantity_each, warehouse) = (params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('warehouse')) + success_count = 0 + skip_count = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_3', ctx.key, {'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_3(ctx: StatefulFunction, func_context, unit_price = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + cost = unit_price * quantity_each + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'process_bulk_orders_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + if __loop_index_1 >= len(customers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'process_bulk_orders_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}, None, reply_to)) + else: + customer = customers[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_balance', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + return send_reply(ctx, reply_to, f"Bulk orders done. Success: {success_count}, Skipped: {skip_count}") + +@marketplace_operator.register +async def process_bulk_orders_step_6(ctx: StatefulFunction, func_context, attr_10 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_10': attr_10, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_stock', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_7(ctx: StatefulFunction, func_context, attr_11 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_10, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('attr_10'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_8', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_10': attr_10, 'attr_11': attr_11, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_8(ctx: StatefulFunction, func_context, attr_12 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_10, attr_11, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('attr_10'), params.get('attr_11'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_9', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_10': attr_10, 'attr_11': attr_11, 'attr_12': attr_12, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'is_active', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_9(ctx: StatefulFunction, func_context, attr_13 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_10, attr_11, attr_12, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('attr_10'), params.get('attr_11'), params.get('attr_12'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_10', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_10': attr_10, 'attr_11': attr_11, 'attr_12': attr_12, 'attr_13': attr_13, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_10(ctx: StatefulFunction, func_context, attr_14 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_10, attr_11, attr_12, attr_13, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('attr_10'), params.get('attr_11'), params.get('attr_12'), params.get('attr_13'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_11', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_10': attr_10, 'attr_11': attr_11, 'attr_12': attr_12, 'attr_13': attr_13, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_product_quantity', key = warehouse, params = (attr_14, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_11(ctx: StatefulFunction, func_context, attr_15 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_10, attr_11, attr_12, attr_13, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('attr_10'), params.get('attr_11'), params.get('attr_12'), params.get('attr_13'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + + if ( + (attr_10 >= cost) + & (attr_11 >= quantity_each) + & attr_12 + & attr_13 + & (attr_15 >= quantity_each) + ): + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_12', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (cost, reply_to)) + else: + skip_count += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'process_bulk_orders_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_12(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_13', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = product, params = (quantity_each, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_13(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_14', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def process_bulk_orders_step_14(ctx: StatefulFunction, func_context, attr_5 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_15', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_5, quantity_each, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_15(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + platform_fee = (cost * __state__['platform_fee_percent']) // 100 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_16', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customer': customer, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'seller', function_name = 'credit_sale', key = seller, params = (cost - platform_fee, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_16(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customer, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customer'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_17', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (cost, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_17(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'process_bulk_orders_step_18', ctx.key, {'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (cost, reply_to)) + +@marketplace_operator.register +async def process_bulk_orders_step_18(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cost, customers, product, quantity_each, seller, skip_count, success_count, warehouse) = (params.get('__loop_index_1'), params.get('cost'), params.get('customers'), params.get('product'), params.get('quantity_each'), params.get('seller'), params.get('skip_count'), params.get('success_count'), params.get('warehouse')) + success_count += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'process_bulk_orders_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cost': cost, 'customers': customers, 'product': product, 'quantity_each': quantity_each, 'seller': seller, 'skip_count': skip_count, 'success_count': success_count, 'warehouse': warehouse}, None, reply_to)) + + +@marketplace_operator.register +async def warehouse_health_check(ctx: StatefulFunction, warehouses: list[str], reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'warehouse_health_check_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def warehouse_health_check_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('warehouses')) + if __loop_index_1 >= len(warehouses): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'warehouse_health_check_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'warehouses': warehouses}, None, reply_to)) + else: + w = warehouses[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'warehouse_health_check_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'calculate_fill_rate', key = w, params = (reply_to,)) + +@marketplace_operator.register +async def warehouse_health_check_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('warehouses')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def warehouse_health_check_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('warehouses')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'warehouse_health_check_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'warehouses': warehouses}, None, reply_to)) + + +@marketplace_operator.register +async def find_overstocked_warehouses( + ctx: StatefulFunction, warehouses: list[str], threshold: int, +reply_to: list = None) -> list[str]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'find_overstocked_warehouses_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'threshold': threshold, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def find_overstocked_warehouses_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, threshold, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('threshold'), params.get('warehouses')) + if __loop_index_1 >= len(warehouses): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'find_overstocked_warehouses_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'threshold': threshold, 'warehouses': warehouses}, None, reply_to)) + else: + w = warehouses[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'find_overstocked_warehouses_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'threshold': threshold, 'w': w, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'calculate_fill_rate', key = w, params = (reply_to,)) + +@marketplace_operator.register +async def find_overstocked_warehouses_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, threshold, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('threshold'), params.get('warehouses')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def find_overstocked_warehouses_step_4(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, threshold, w, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('threshold'), params.get('w'), params.get('warehouses')) + if attr_2 > threshold: + _comp_result_1.append(w) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'find_overstocked_warehouses_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'threshold': threshold, 'warehouses': warehouses}, None, reply_to)) + + +@marketplace_operator.register +async def seller_revenue_summary(ctx: StatefulFunction, sellers: list[str], reply_to: list = None) -> dict[str, int]: + _comp_result_1 = {} + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'seller_revenue_summary_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def seller_revenue_summary_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('sellers')) + if __loop_index_1 >= len(sellers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'seller_revenue_summary_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + else: + s = sellers[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'seller_revenue_summary_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 's': s, 'sellers': sellers}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_revenue', key = s, params = (reply_to,)) + +@marketplace_operator.register +async def seller_revenue_summary_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('sellers')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def seller_revenue_summary_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, s, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('s'), params.get('sellers')) + _comp_result_1[s] = attr_1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'seller_revenue_summary_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + + +@marketplace_operator.register +async def rank_products_by_popularity(ctx: StatefulFunction, products: list[str], reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'rank_products_by_popularity_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def rank_products_by_popularity_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'rank_products_by_popularity_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'rank_products_by_popularity_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_popularity_score', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def rank_products_by_popularity_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + scores = _comp_result_1 + scores.sort(reverse=True) + return send_reply(ctx, reply_to, scores) + +@marketplace_operator.register +async def rank_products_by_popularity_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'rank_products_by_popularity_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def total_platform_earnings_from_sellers(ctx: StatefulFunction, sellers: list[str], reply_to: list = None) -> int: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_platform_earnings_from_sellers_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def total_platform_earnings_from_sellers_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('sellers')) + if __loop_index_1 >= len(sellers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_platform_earnings_from_sellers_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + else: + s = sellers[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'total_platform_earnings_from_sellers_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_revenue', key = s, params = (reply_to,)) + +@marketplace_operator.register +async def total_platform_earnings_from_sellers_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('sellers')) + total = sum( + _comp_result_1 + ) + return send_reply(ctx, reply_to, total) + +@marketplace_operator.register +async def total_platform_earnings_from_sellers_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('sellers')) + _comp_result_1.append((attr_1 * __state__['platform_fee_percent']) // 100) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'total_platform_earnings_from_sellers_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'sellers': sellers}, None, reply_to)) + + + +@marketplace_operator.register +async def multi_product_availability_check( + ctx: StatefulFunction, products: list[str], quantities: list[int], +reply_to: list = None) -> bool: + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'multi_product_availability_check_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities}, None, reply_to)) + +@marketplace_operator.register +async def multi_product_availability_check_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'multi_product_availability_check_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + p = products[i] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'multi_product_availability_check_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'i': i, 'p': p, 'products': products, 'quantities': quantities}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_stock', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def multi_product_availability_check_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities')) + return send_reply(ctx, reply_to, True) + +@marketplace_operator.register +async def multi_product_availability_check_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, i, p, products, quantities) = (params.get('__loop_index_1'), params.get('i'), params.get('p'), params.get('products'), params.get('quantities')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'multi_product_availability_check_step_5', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_1': attr_1, 'i': i, 'p': p, 'products': products, 'quantities': quantities}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def multi_product_availability_check_step_5(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_1, i, p, products, quantities) = (params.get('__loop_index_1'), params.get('attr_1'), params.get('i'), params.get('p'), params.get('products'), params.get('quantities')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'multi_product_availability_check_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_1': attr_1, 'attr_2': attr_2, 'i': i, 'products': products, 'quantities': quantities}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def multi_product_availability_check_step_6(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_1, attr_2, i, products, quantities) = (params.get('__loop_index_1'), params.get('attr_1'), params.get('attr_2'), params.get('i'), params.get('products'), params.get('quantities')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'multi_product_availability_check_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'attr_1': attr_1, 'attr_2': attr_2, 'i': i, 'products': products, 'quantities': quantities}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'is_active', key = attr_3, params = (reply_to,)) + +@marketplace_operator.register +async def multi_product_availability_check_step_7(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_1, attr_2, i, products, quantities) = (params.get('__loop_index_1'), params.get('attr_1'), params.get('attr_2'), params.get('i'), params.get('products'), params.get('quantities')) + if ( + (attr_1 < quantities[i]) + | (not attr_2) + | (not attr_4) + ): + return send_reply(ctx, reply_to, False) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'multi_product_availability_check_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities}, None, reply_to)) + + +@marketplace_operator.register +async def compute_order_breakdown( + ctx: StatefulFunction, products: list[str], + quantities: list[int], + coupon: str, +reply_to: list = None) -> tuple[int, int, int]: + subtotal = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_order_breakdown_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupon': coupon, 'products': products, 'quantities': quantities, 'subtotal': subtotal}, None, reply_to)) + +@marketplace_operator.register +async def compute_order_breakdown_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, coupon, products, quantities, subtotal) = (params.get('__loop_index_1'), params.get('coupon'), params.get('products'), params.get('quantities'), params.get('subtotal')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_order_breakdown_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupon': coupon, 'products': products, 'quantities': quantities, 'subtotal': subtotal}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + attr_1 = products[i] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'compute_order_breakdown_step_5', ctx.key, {'__loop_index_1': __loop_index_1, 'coupon': coupon, 'i': i, 'products': products, 'quantities': quantities, 'subtotal': subtotal}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = attr_1, params = (reply_to,)) + +@marketplace_operator.register +async def compute_order_breakdown_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, coupon, products, quantities, subtotal) = (params.get('__loop_index_1'), params.get('coupon'), params.get('products'), params.get('quantities'), params.get('subtotal')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'compute_order_breakdown_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'subtotal': subtotal}) + ctx.call_remote_async(operator_name = 'coupon', function_name = 'apply', key = coupon, params = (subtotal, reply_to)) + +@marketplace_operator.register +async def compute_order_breakdown_step_4(ctx: StatefulFunction, func_context, discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, subtotal) = (params.get('__loop_index_1'), params.get('subtotal')) + final = subtotal - discount + return send_reply(ctx, reply_to, (subtotal, discount, final)) + +@marketplace_operator.register +async def compute_order_breakdown_step_5(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, coupon, i, products, quantities, subtotal) = (params.get('__loop_index_1'), params.get('coupon'), params.get('i'), params.get('products'), params.get('quantities'), params.get('subtotal')) + subtotal += attr_2 * quantities[i] + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_order_breakdown_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupon': coupon, 'products': products, 'quantities': quantities, 'subtotal': subtotal}, None, reply_to)) + + +@marketplace_operator.register +async def loyalty_cashback_campaign( + ctx: StatefulFunction, customers: list[str], products: list[str], +reply_to: list = None) -> int: + active_count = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'active_count': active_count, 'customers': customers, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, active_count, customers, products) = (params.get('__loop_index_1'), params.get('active_count'), params.get('customers'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'active_count': active_count, 'customers': customers, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'loyalty_cashback_campaign_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'active_count': active_count, 'customers': customers, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, active_count, customers, products) = (params.get('__loop_index_1'), params.get('active_count'), params.get('customers'), params.get('products')) + total_points_granted = 0 + __loop_index_2 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'active_count': active_count, 'customers': customers, 'total_points_granted': total_points_granted}, None, reply_to)) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, active_count, customers, total_points_granted) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('active_count'), params.get('customers'), params.get('total_points_granted')) + if __loop_index_2 >= len(customers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'active_count': active_count, 'customers': customers, 'total_points_granted': total_points_granted}, None, reply_to)) + else: + c = customers[__loop_index_2] + __loop_index_2 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'loyalty_cashback_campaign_step_6', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'active_count': active_count, 'customers': customers, 'total_points_granted': total_points_granted}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = c, params = (active_count * 100, reply_to)) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, active_count, customers, total_points_granted) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('active_count'), params.get('customers'), params.get('total_points_granted')) + return send_reply(ctx, reply_to, total_points_granted) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, active_count, customers, total_points_granted) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('active_count'), params.get('customers'), params.get('total_points_granted')) + total_points_granted += active_count + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'active_count': active_count, 'customers': customers, 'total_points_granted': total_points_granted}, None, reply_to)) + +@marketplace_operator.register +async def loyalty_cashback_campaign_step_7(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, active_count, customers, products) = (params.get('__loop_index_1'), params.get('active_count'), params.get('customers'), params.get('products')) + if attr_1: + active_count += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'loyalty_cashback_campaign_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'active_count': active_count, 'customers': customers, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def get_seller_product_prices( + ctx: StatefulFunction, seller: str, products: list[str], +reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_seller_product_prices_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'seller': seller}, None, reply_to)) + +@marketplace_operator.register +async def get_seller_product_prices_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, seller) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('seller')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_seller_product_prices_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'seller': seller}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_seller_product_prices_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'p': p, 'products': products, 'seller': seller}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_seller_product_prices_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, seller) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('seller')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def get_seller_product_prices_step_4(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, p, products, seller) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('p'), params.get('products'), params.get('seller')) + if attr_3 == seller: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_seller_product_prices_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'seller': seller}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_seller_product_prices_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'seller': seller}, None, reply_to)) + +@marketplace_operator.register +async def get_seller_product_prices_step_5(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, seller) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('seller')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_seller_product_prices_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'seller': seller}, None, reply_to)) + + +@marketplace_operator.register +async def cross_entity_stats( + ctx: StatefulFunction, sellers: list[str], + customers: list[str], + products: list[str], + warehouses: list[str], +reply_to: list = None) -> str: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'customers': customers, 'products': products, 'sellers': sellers, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, customers, products, sellers, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('customers'), params.get('products'), params.get('sellers'), params.get('warehouses')) + if __loop_index_1 >= len(sellers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'customers': customers, 'products': products, 'sellers': sellers, 'warehouses': warehouses}, None, reply_to)) + else: + s = sellers[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'cross_entity_stats_step_16', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'customers': customers, 'products': products, 'sellers': sellers, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_balance', key = s, params = (reply_to,)) + +@marketplace_operator.register +async def cross_entity_stats_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, customers, products, sellers, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('customers'), params.get('products'), params.get('sellers'), params.get('warehouses')) + total_seller_balance = sum(_comp_result_1) + _comp_result_2 = [] + __loop_index_2 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'customers': customers, 'products': products, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, customers, products, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('customers'), params.get('products'), params.get('total_seller_balance'), params.get('warehouses')) + if __loop_index_2 >= len(customers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'customers': customers, 'products': products, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + else: + c = customers[__loop_index_2] + __loop_index_2 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'cross_entity_stats_step_15', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'customers': customers, 'products': products, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_balance', key = c, params = (reply_to,)) + +@marketplace_operator.register +async def cross_entity_stats_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, customers, products, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('customers'), params.get('products'), params.get('total_seller_balance'), params.get('warehouses')) + total_customer_balance = sum(_comp_result_2) + _comp_result_3 = [] + __loop_index_3 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'products': products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + if __loop_index_3 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_7', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'products': products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + else: + p = products[__loop_index_3] + __loop_index_3 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'cross_entity_stats_step_14', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'p': p, 'products': products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'product', function_name = 'is_available', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def cross_entity_stats_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + active_products = _comp_result_3 + _comp_result_4 = [] + __loop_index_4 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_8', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, 'active_products': active_products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_8(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, active_products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('active_products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + if __loop_index_4 >= len(active_products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_9', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, 'active_products': active_products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + else: + p = active_products[__loop_index_4] + __loop_index_4 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'cross_entity_stats_step_13', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, 'active_products': active_products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_stock', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def cross_entity_stats_step_9(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, active_products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('active_products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + avg_stock = sum(_comp_result_4) + _comp_result_5 = [] + __loop_index_5 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_10', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '__loop_index_5': __loop_index_5, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, '_comp_result_5': _comp_result_5, 'active_products': active_products, 'avg_stock': avg_stock, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_10(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, __loop_index_5, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, _comp_result_5, active_products, avg_stock, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('__loop_index_5'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('_comp_result_5'), params.get('active_products'), params.get('avg_stock'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + if __loop_index_5 >= len(warehouses): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_11', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '__loop_index_5': __loop_index_5, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, '_comp_result_5': _comp_result_5, 'active_products': active_products, 'avg_stock': avg_stock, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + else: + w = warehouses[__loop_index_5] + __loop_index_5 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'cross_entity_stats_step_12', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '__loop_index_5': __loop_index_5, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, '_comp_result_5': _comp_result_5, 'active_products': active_products, 'avg_stock': avg_stock, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'calculate_fill_rate', key = w, params = (reply_to,)) + +@marketplace_operator.register +async def cross_entity_stats_step_11(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, __loop_index_5, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, _comp_result_5, active_products, avg_stock, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('__loop_index_5'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('_comp_result_5'), params.get('active_products'), params.get('avg_stock'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + warehouse_fill_rates = _comp_result_5 + avg_fill = sum(warehouse_fill_rates) // len(warehouse_fill_rates) if warehouse_fill_rates else 0 + return send_reply(ctx, reply_to, ( + "Sellers balance: " + str(total_seller_balance) + + " | Customers balance: " + str(total_customer_balance) + + " | Active products: " + str(len(active_products)) + + " | Total stock: " + str(avg_stock) + + " | Avg warehouse fill: " + str(avg_fill) + "%" + )) + +@marketplace_operator.register +async def cross_entity_stats_step_12(ctx: StatefulFunction, func_context, attr_9 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, __loop_index_5, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, _comp_result_5, active_products, avg_stock, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('__loop_index_5'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('_comp_result_5'), params.get('active_products'), params.get('avg_stock'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + _comp_result_5.append(attr_9) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_10', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '__loop_index_5': __loop_index_5, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, '_comp_result_5': _comp_result_5, 'active_products': active_products, 'avg_stock': avg_stock, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_13(ctx: StatefulFunction, func_context, attr_7 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, __loop_index_4, _comp_result_1, _comp_result_2, _comp_result_3, _comp_result_4, active_products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('__loop_index_4'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('_comp_result_4'), params.get('active_products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + _comp_result_4.append(attr_7) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_8', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '__loop_index_4': __loop_index_4, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, '_comp_result_4': _comp_result_4, 'active_products': active_products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_14(ctx: StatefulFunction, func_context, attr_6 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, p, products, total_customer_balance, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('p'), params.get('products'), params.get('total_customer_balance'), params.get('total_seller_balance'), params.get('warehouses')) + if attr_6: + _comp_result_3.append(p) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'products': products, 'total_customer_balance': total_customer_balance, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_15(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, customers, products, total_seller_balance, warehouses) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('customers'), params.get('products'), params.get('total_seller_balance'), params.get('warehouses')) + _comp_result_2.append(attr_3) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'customers': customers, 'products': products, 'total_seller_balance': total_seller_balance, 'warehouses': warehouses}, None, reply_to)) + +@marketplace_operator.register +async def cross_entity_stats_step_16(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, customers, products, sellers, warehouses) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('customers'), params.get('products'), params.get('sellers'), params.get('warehouses')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'cross_entity_stats_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'customers': customers, 'products': products, 'sellers': sellers, 'warehouses': warehouses}, None, reply_to)) + + +@marketplace_operator.register +async def fire_restock_notifications( + ctx: StatefulFunction, products: list[str], threshold: int, +reply_to: list = None) -> None: + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'fire_restock_notifications_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'threshold': threshold}, None, reply_to)) + +@marketplace_operator.register +async def fire_restock_notifications_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, attr_1, products, threshold) = (params.get('__loop_index_1'), params.get('attr_1'), params.get('products'), params.get('threshold')) + if __loop_index_1 >= len(products): + return send_reply(ctx, reply_to, None) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'fire_restock_notifications_step_3', ctx.key, {'__loop_index_1': __loop_index_1, 'p': p, 'products': products, 'threshold': threshold}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_stock', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def fire_restock_notifications_step_3(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, p, products, threshold) = (params.get('__loop_index_1'), params.get('p'), params.get('products'), params.get('threshold')) + if attr_1 < threshold: + ctx.call_remote_async(operator_name = 'product', function_name = 'add_tag', key = p, params = ("low_stock", [{'sink': True}])) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'fire_restock_notifications_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'attr_1': attr_1, 'products': products, 'threshold': threshold}, None, reply_to)) + + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon( + ctx: StatefulFunction, customer: str, + products: list[str], + quantities: list[int], + coupon: str, + warehouse: str, +reply_to: list = None) -> str: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_2', ctx.key, {'coupon': coupon, 'customer': customer, 'products': products, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'compute_cart_total', key = ctx.key, params = (customer, products, quantities, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_2(ctx: StatefulFunction, func_context, subtotal = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (coupon, customer, products, quantities, warehouse) = (params.get('coupon'), params.get('customer'), params.get('products'), params.get('quantities'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_3', ctx.key, {'customer': customer, 'products': products, 'quantities': quantities, 'subtotal': subtotal, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'coupon', function_name = 'apply', key = coupon, params = (subtotal, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_3(ctx: StatefulFunction, func_context, discount = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, products, quantities, subtotal, warehouse) = (params.get('customer'), params.get('products'), params.get('quantities'), params.get('subtotal'), params.get('warehouse')) + after_coupon = subtotal - discount + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_4', ctx.key, {'after_coupon': after_coupon, 'customer': customer, 'products': products, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_loyalty_points', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_4(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (after_coupon, customer, products, quantities, warehouse) = (params.get('after_coupon'), params.get('customer'), params.get('products'), params.get('quantities'), params.get('warehouse')) + points_to_redeem = attr_3 // 2 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_5', ctx.key, {'after_coupon': after_coupon, 'customer': customer, 'products': products, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'redeem_loyalty_points', key = customer, params = (points_to_redeem, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_5(ctx: StatefulFunction, func_context, cashback = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (after_coupon, customer, products, quantities, warehouse) = (params.get('after_coupon'), params.get('customer'), params.get('products'), params.get('quantities'), params.get('warehouse')) + final = after_coupon - cashback + if final < 0: + final = 0 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_6', ctx.key, {'customer': customer, 'final': final, 'products': products, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_balance', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_6(ctx: StatefulFunction, func_context, attr_5 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (customer, final, products, quantities, warehouse) = (params.get('customer'), params.get('final'), params.get('products'), params.get('quantities'), params.get('warehouse')) + + if attr_5 < final: + raise InsufficientFunds("Customer cannot afford cart after discounts.") + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'checkout_with_loyalty_and_coupon_step_7', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final, 'products': products, 'quantities': quantities, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, customer, final, products, quantities, warehouse) = (params.get('__loop_index_1'), params.get('customer'), params.get('final'), params.get('products'), params.get('quantities'), params.get('warehouse')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'checkout_with_loyalty_and_coupon_step_8', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final, 'products': products, 'quantities': quantities, 'warehouse': warehouse}, None, reply_to)) + else: + i = __loop_index_1 + __loop_index_1 += 1 + p = products[i] + qty = quantities[i] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_12', ctx.key, {'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final, 'p': p, 'products': products, 'qty': qty, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'deduct_stock', key = p, params = (qty, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_8(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, customer, final, products, quantities, warehouse) = (params.get('__loop_index_1'), params.get('customer'), params.get('final'), params.get('products'), params.get('quantities'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_9', ctx.key, {'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'deduct_funds', key = customer, params = (final, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_9(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, customer, final) = (params.get('__loop_index_1'), params.get('customer'), params.get('final')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_10', ctx.key, {'__loop_index_1': __loop_index_1, 'final': final}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'earn_loyalty_points', key = customer, params = (final, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_10(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, final) = (params.get('__loop_index_1'), params.get('final')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_11', ctx.key, {'__loop_index_1': __loop_index_1, 'final': final}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'record_transaction', key = ctx.key, params = (final, reply_to)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_11(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, final) = (params.get('__loop_index_1'), params.get('final')) + return send_reply(ctx, reply_to, "Checkout complete. Paid: " + str(final) + ", Loyalty earned: " + str(final // 100)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_12(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, customer, final, p, products, qty, quantities, warehouse) = (params.get('__loop_index_1'), params.get('customer'), params.get('final'), params.get('p'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_13', ctx.key, {'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final, 'products': products, 'qty': qty, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def checkout_with_loyalty_and_coupon_step_13(ctx: StatefulFunction, func_context, attr_7 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, customer, final, products, qty, quantities, warehouse) = (params.get('__loop_index_1'), params.get('customer'), params.get('final'), params.get('products'), params.get('qty'), params.get('quantities'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'checkout_with_loyalty_and_coupon_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'customer': customer, 'final': final, 'products': products, 'quantities': quantities, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = warehouse, params = (attr_7, qty, reply_to)) + + +@marketplace_operator.register +async def recursive_price_sum(ctx: StatefulFunction, products: list[str], reply_to: list = None) -> int: + if not products: + return send_reply(ctx, reply_to, 0) + attr_1 = products[0] + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'recursive_price_sum_step_2', ctx.key, {'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = attr_1, params = (reply_to,)) + +@marketplace_operator.register +async def recursive_price_sum_step_2(ctx: StatefulFunction, func_context, head_price = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products,) = (params.get('products'),) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'recursive_price_sum_step_3', ctx.key, {'head_price': head_price}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'recursive_price_sum', key = ctx.key, params = (products[1:], reply_to)) + +@marketplace_operator.register +async def recursive_price_sum_step_3(ctx: StatefulFunction, func_context, rest_sum = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (head_price,) = (params.get('head_price'),) + return send_reply(ctx, reply_to, head_price + rest_sum) + + +@marketplace_operator.register +async def tag_popular_products( + ctx: StatefulFunction, products: list[str], score_threshold: int, +reply_to: list = None) -> int: + tagged = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'tag_popular_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'score_threshold': score_threshold, 'tagged': tagged}, None, reply_to)) + +@marketplace_operator.register +async def tag_popular_products_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, score, score_threshold, tagged) = (params.get('__loop_index_1'), params.get('products'), params.get('score'), params.get('score_threshold'), params.get('tagged')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'tag_popular_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'score': score, 'score_threshold': score_threshold, 'tagged': tagged}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'tag_popular_products_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'p': p, 'products': products, 'score_threshold': score_threshold, 'tagged': tagged}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_popularity_score', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def tag_popular_products_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, score, score_threshold, tagged) = (params.get('__loop_index_1'), params.get('products'), params.get('score'), params.get('score_threshold'), params.get('tagged')) + return send_reply(ctx, reply_to, tagged) + +@marketplace_operator.register +async def tag_popular_products_step_4(ctx: StatefulFunction, func_context, score = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, p, products, score_threshold, tagged) = (params.get('__loop_index_1'), params.get('p'), params.get('products'), params.get('score_threshold'), params.get('tagged')) + if score >= score_threshold: + ctx.call_remote_async(operator_name = 'product', function_name = 'add_tag', key = p, params = ("trending", [{'sink': True}])) + tagged += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'tag_popular_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'score': score, 'score_threshold': score_threshold, 'tagged': tagged}, None, reply_to)) + + +@marketplace_operator.register +async def get_customer_cart_value( + ctx: StatefulFunction, customer: str, products: list[str], +reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_customer_cart_value_step_2', ctx.key, {'products': products}) + ctx.call_remote_async(operator_name = 'customer', function_name = 'get_cart', key = customer, params = (reply_to,)) + +@marketplace_operator.register +async def get_customer_cart_value_step_2(ctx: StatefulFunction, func_context, cart = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products,) = (params.get('products'),) + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_customer_cart_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def get_customer_cart_value_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, cart, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('cart'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_customer_cart_value_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_customer_cart_value_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'p': p, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_customer_cart_value_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, cart, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('cart'), params.get('products')) + total = sum(_comp_result_1) + return send_reply(ctx, reply_to, total) + +@marketplace_operator.register +async def get_customer_cart_value_step_5(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, cart, p, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('cart'), params.get('p'), params.get('products')) + if attr_4 in cart: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_customer_cart_value_step_6', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_customer_cart_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def get_customer_cart_value_step_6(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, cart, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('cart'), params.get('products')) + _comp_result_1.append(attr_2) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_customer_cart_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'cart': cart, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def rebalance_warehouses( + ctx: StatefulFunction, source: str, + destination: str, + product_id: str, + transfer_qty: int, +reply_to: list = None) -> str: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'rebalance_warehouses_step_2', ctx.key, {'destination': destination, 'product_id': product_id, 'source': source, 'transfer_qty': transfer_qty}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_product_quantity', key = source, params = (product_id, reply_to)) + +@marketplace_operator.register +async def rebalance_warehouses_step_2(ctx: StatefulFunction, func_context, available_in_source = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (destination, product_id, source, transfer_qty) = (params.get('destination'), params.get('product_id'), params.get('source'), params.get('transfer_qty')) + if available_in_source < transfer_qty: + raise InsufficientStock("Source warehouse does not have enough stock.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'rebalance_warehouses_step_3', ctx.key, {'destination': destination, 'product_id': product_id, 'source': source, 'transfer_qty': transfer_qty}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'get_available_capacity', key = destination, params = (reply_to,)) + +@marketplace_operator.register +async def rebalance_warehouses_step_3(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (destination, product_id, source, transfer_qty) = (params.get('destination'), params.get('product_id'), params.get('source'), params.get('transfer_qty')) + + if attr_2 < transfer_qty: + raise WarehouseCapacityExceeded("Destination warehouse cannot fit the transfer.") + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'rebalance_warehouses_step_4', ctx.key, {'destination': destination, 'product_id': product_id, 'transfer_qty': transfer_qty}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'remove_product', key = source, params = (product_id, transfer_qty, reply_to)) + +@marketplace_operator.register +async def rebalance_warehouses_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (destination, product_id, transfer_qty) = (params.get('destination'), params.get('product_id'), params.get('transfer_qty')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'rebalance_warehouses_step_5', ctx.key, {'product_id': product_id, 'transfer_qty': transfer_qty}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'store_product', key = destination, params = (product_id, transfer_qty, reply_to)) + +@marketplace_operator.register +async def rebalance_warehouses_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (product_id, transfer_qty) = (params.get('product_id'), params.get('transfer_qty')) + return send_reply(ctx, reply_to, "Transferred " + str(transfer_qty) + " units of " + product_id) + + +@marketplace_operator.register +async def full_seller_onboarding( + ctx: StatefulFunction, seller: str, + products: list[str], + quantities: list[int], + warehouse: str, +reply_to: list = None) -> str: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_2', ctx.key, {'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def full_seller_onboarding_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products, quantities, seller, warehouse) = (params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_3', ctx.key, {'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'register_seller', key = ctx.key, params = (attr_1, reply_to)) + +@marketplace_operator.register +async def full_seller_onboarding_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (products, quantities, seller, warehouse) = (params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'full_seller_onboarding_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def full_seller_onboarding_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, seller, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'full_seller_onboarding_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_8', ctx.key, {'__loop_index_1': __loop_index_1, 'p': p, 'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'add_product', key = seller, params = (p, reply_to)) + +@marketplace_operator.register +async def full_seller_onboarding_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, seller, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_6', ctx.key, {'__loop_index_1': __loop_index_1, 'seller': seller}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'batch_restock', key = ctx.key, params = (products, quantities, warehouse, reply_to)) + +@marketplace_operator.register +async def full_seller_onboarding_step_6(ctx: StatefulFunction, func_context, result = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, seller) = (params.get('__loop_index_1'), params.get('seller')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'result': result}) + ctx.call_remote_async(operator_name = 'seller', function_name = 'get_seller_id', key = seller, params = (reply_to,)) + +@marketplace_operator.register +async def full_seller_onboarding_step_7(ctx: StatefulFunction, func_context, attr_7 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, result) = (params.get('__loop_index_1'), params.get('result')) + return send_reply(ctx, reply_to, "Onboarded seller " + attr_7 + ". " + result) + +@marketplace_operator.register +async def full_seller_onboarding_step_8(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, p, products, quantities, seller, warehouse) = (params.get('__loop_index_1'), params.get('p'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_9', ctx.key, {'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def full_seller_onboarding_step_9(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, products, quantities, seller, warehouse) = (params.get('__loop_index_1'), params.get('products'), params.get('quantities'), params.get('seller'), params.get('warehouse')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'full_seller_onboarding_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'products': products, 'quantities': quantities, 'seller': seller, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'list_product', key = ctx.key, params = (attr_4, reply_to)) + + +@marketplace_operator.register +async def get_product_dict(ctx: StatefulFunction, products: list[str], reply_to: list = None) -> dict[str, int]: + _comp_result_1 = {} + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_product_dict_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def get_product_dict_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_product_dict_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_product_dict_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'p': p, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_product_id', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_product_dict_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def get_product_dict_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, p, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('p'), params.get('products')) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_product_dict_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'attr_1': attr_1, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def get_product_dict_step_5(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, attr_1, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('attr_1'), params.get('products')) + _comp_result_1[attr_1] = attr_2 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_product_dict_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def count_high_rated_products( + ctx: StatefulFunction, products: list[str], min_rating: int, +reply_to: list = None) -> int: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'count_high_rated_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'min_rating': min_rating, 'products': products}, None, reply_to)) + +@marketplace_operator.register +async def count_high_rated_products_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, min_rating, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('min_rating'), params.get('products')) + if __loop_index_1 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'count_high_rated_products_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'min_rating': min_rating, 'products': products}, None, reply_to)) + else: + p = products[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'count_high_rated_products_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'min_rating': min_rating, 'p': p, 'products': products}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_average_rating', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def count_high_rated_products_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, min_rating, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('min_rating'), params.get('products')) + high_rated = _comp_result_1 + return send_reply(ctx, reply_to, len(high_rated)) + +@marketplace_operator.register +async def count_high_rated_products_step_4(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, min_rating, p, products) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('min_rating'), params.get('p'), params.get('products')) + if attr_2 >= min_rating: + _comp_result_1.append(p) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'count_high_rated_products_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'min_rating': min_rating, 'products': products}, None, reply_to)) + + +@marketplace_operator.register +async def get_ret_tuple(ctx: StatefulFunction, product: str, reply_to: list = None) -> tuple[int, int]: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_ret_tuple_step_2', ctx.key, {'product': product}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def get_ret_tuple_step_2(ctx: StatefulFunction, func_context, price = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (product,) = (params.get('product'),) + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'get_ret_tuple_step_3', ctx.key, {'price': price}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_stock', key = product, params = (reply_to,)) + +@marketplace_operator.register +async def get_ret_tuple_step_3(ctx: StatefulFunction, func_context, stock = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (price,) = (params.get('price'),) + return send_reply(ctx, reply_to, (price, stock)) + + +@marketplace_operator.register +async def unpack_and_use_tuple(ctx: StatefulFunction, product: str, reply_to: list = None) -> str: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'unpack_and_use_tuple_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'get_ret_tuple', key = ctx.key, params = (product, reply_to)) + +@marketplace_operator.register +async def unpack_and_use_tuple_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + price, stock = attr_1 + return send_reply(ctx, reply_to, "Price: " + str(price) + ", Stock: " + str(stock)) + + +@marketplace_operator.register +async def nested_comprehension_test( + ctx: StatefulFunction, sellers: list[str], products: list[str], +reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def nested_comprehension_test_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('sellers')) + if __loop_index_1 >= len(sellers): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'products': products, 'sellers': sellers}, None, reply_to)) + else: + s = sellers[__loop_index_1] + __loop_index_1 += 1 + _comp_result_2 = [] + __loop_index_2 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 's': s, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def nested_comprehension_test_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, products, sellers) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('products'), params.get('sellers')) + return send_reply(ctx, reply_to, _comp_result_1) + +@marketplace_operator.register +async def nested_comprehension_test_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, products, s, sellers) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('products'), params.get('s'), params.get('sellers')) + if __loop_index_2 >= len(products): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 's': s, 'sellers': sellers}, None, reply_to)) + else: + p = products[__loop_index_2] + __loop_index_2 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'nested_comprehension_test_step_6', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'p': p, 'products': products, 's': s, 'sellers': sellers}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_seller', key = p, params = (reply_to,)) + +@marketplace_operator.register +async def nested_comprehension_test_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, products, s, sellers) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('products'), params.get('s'), params.get('sellers')) + _comp_result_1.append(sum(_comp_result_2)) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def nested_comprehension_test_step_6(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, p, products, s, sellers) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('p'), params.get('products'), params.get('s'), params.get('sellers')) + if attr_3 == s: + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'nested_comprehension_test_step_7', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 's': s, 'sellers': sellers}) + ctx.call_remote_async(operator_name = 'product', function_name = 'get_price', key = p, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 's': s, 'sellers': sellers}, None, reply_to)) + +@marketplace_operator.register +async def nested_comprehension_test_step_7(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, products, s, sellers) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('products'), params.get('s'), params.get('sellers')) + _comp_result_2.append(attr_1) + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'nested_comprehension_test_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'products': products, 's': s, 'sellers': sellers}, None, reply_to)) + + +@marketplace_operator.register +async def dispatch_all_pending(ctx: StatefulFunction, warehouse: str, order_ids: list[str], reply_to: list = None) -> int: + dispatched = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'dispatch_all_pending_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'dispatched': dispatched, 'order_ids': order_ids, 'warehouse': warehouse}, None, reply_to)) + +@marketplace_operator.register +async def dispatch_all_pending_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, dispatched, order_ids, warehouse) = (params.get('__loop_index_1'), params.get('dispatched'), params.get('order_ids'), params.get('warehouse')) + if __loop_index_1 >= len(order_ids): + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'dispatch_all_pending_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'dispatched': dispatched, 'order_ids': order_ids, 'warehouse': warehouse}, None, reply_to)) + else: + order_id = order_ids[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'marketplace', 'dispatch_all_pending_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'dispatched': dispatched, 'order_ids': order_ids, 'warehouse': warehouse}) + ctx.call_remote_async(operator_name = 'warehouse', function_name = 'dispatch_shipment', key = warehouse, params = (order_id, reply_to)) + +@marketplace_operator.register +async def dispatch_all_pending_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, dispatched, order_ids, warehouse) = (params.get('__loop_index_1'), params.get('dispatched'), params.get('order_ids'), params.get('warehouse')) + return send_reply(ctx, reply_to, dispatched) + +@marketplace_operator.register +async def dispatch_all_pending_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, dispatched, order_ids, warehouse) = (params.get('__loop_index_1'), params.get('dispatched'), params.get('order_ids'), params.get('warehouse')) + dispatched += 1 + ctx.call_remote_async(operator_name = 'marketplace', function_name = 'dispatch_all_pending_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'dispatched': dispatched, 'order_ids': order_ids, 'warehouse': warehouse}, None, reply_to)) + diff --git a/obol/examples/compiled/user_item.py b/obol/examples/compiled/user_item.py new file mode 100644 index 00000000..bd83d233 --- /dev/null +++ b/obol/examples/compiled/user_item.py @@ -0,0 +1,1184 @@ +from styx.common.operator import Operator +from styx.common.stateful_function import StatefulFunction +from styx.common.logging import logging + +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + if barrier["total"] == 0: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + return True, (), barrier["saved"], barrier["parent_reply_to"] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None + +from typing import Optional +import logging +from typing import TypeVar, Type, Callable, Any + + + +class NotEnoughBalance(Exception): + pass + + +class OutOfStock(Exception): + pass +coupon_operator = Operator('coupon', n_partitions=4) + +@coupon_operator.register +async def insert(ctx: StatefulFunction, code: str, discount: int, reply_to: list = None): + __state__ = {} + __state__['code'] = code + __state__['discount'] = discount + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@coupon_operator.register +async def get_discount(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['discount']) + +item_operator = Operator('item', n_partitions=4) + +@item_operator.register +async def insert(ctx: StatefulFunction, item_name: str, price: int, reply_to: list = None): + __state__ = {} + __state__['item_name'] = item_name + __state__['stock'] = 0 + __state__['price'] = price + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@item_operator.register +async def get_price(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['price']) + + +@item_operator.register +async def get_stock(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['stock']) + + +@item_operator.register +async def update_stock(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if (__state__['stock'] + amount) < 0: + raise OutOfStock("Not enough stock to update.") + __state__['stock'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + +user_operator = Operator('user', n_partitions=4) + +@user_operator.register +async def insert(ctx: StatefulFunction, username: str, reply_to: list = None): + __state__ = {} + __state__['username'] = username + __state__['balance'] = 0 + __state__['myitems'] = [] + ctx.put_func_context({}) + ctx.put(__state__) + return send_reply(ctx, reply_to, ctx.key) + + +@user_operator.register +async def get_balance(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['balance']) + + +@user_operator.register +async def get_items(ctx: StatefulFunction, reply_to: list = None) -> list[str]: + __state__ = ctx.get() or {} + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['myitems']) + + +@user_operator.register +async def add_balance(ctx: StatefulFunction, amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + __state__['balance'] += amount + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + + +@user_operator.register +async def simple_loop(ctx: StatefulFunction, items: list[str], reply_to: list = None) -> int: + total = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'simple_loop_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'items': items, 'total': total}, None, reply_to)) + +@user_operator.register +async def simple_loop_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, items, total) = (params.get('__loop_index_1'), params.get('items'), params.get('total')) + if __loop_index_1 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'simple_loop_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'items': items, 'total': total}, None, reply_to)) + else: + item = items[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'simple_loop_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'items': items, 'total': total}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def simple_loop_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, items, total) = (params.get('__loop_index_1'), params.get('items'), params.get('total')) + return send_reply(ctx, reply_to, total) + +@user_operator.register +async def simple_loop_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, items, total) = (params.get('__loop_index_1'), params.get('items'), params.get('total')) + total += attr_1 + ctx.call_remote_async(operator_name = 'user', function_name = 'simple_loop_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'items': items, 'total': total}, None, reply_to)) + + +@user_operator.register +async def buy_item(ctx: StatefulFunction, amount: int, item: str, reply_to: list = None) -> bool: + reply_to = push_continuation(ctx, reply_to, 'user', 'buy_item_step_2', ctx.key, {'amount': amount, 'item': item}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def buy_item_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (amount, item) = (params.get('amount'), params.get('item')) + total_price = amount * attr_1 + + if __state__['balance'] < total_price: + raise NotEnoughBalance("Not enough balance to buy the item.") + reply_to = push_continuation(ctx, reply_to, 'user', 'buy_item_step_3', ctx.key, {'item': item, 'total_price': total_price}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (-amount, reply_to)) + +@user_operator.register +async def buy_item_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (item, total_price) = (params.get('item'), params.get('total_price')) + __state__['balance'] -= total_price + __state__['myitems'].append(item) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + +@user_operator.register +async def drain_stock(ctx: StatefulFunction, item: str, reply_to: list = None) -> int: + total = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'drain_stock_step_2', key = ctx.key, params = ({'item': item, 'total': total}, None, reply_to)) + +@user_operator.register +async def drain_stock_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (item, total) = (params.get('item'), params.get('total')) + reply_to = push_continuation(ctx, reply_to, 'user', 'drain_stock_step_4', ctx.key, {'item': item, 'total': total}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + +@user_operator.register +async def drain_stock_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (item, total) = (params.get('item'), params.get('total')) + return send_reply(ctx, reply_to, total) + +@user_operator.register +async def drain_stock_step_4(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (item, total) = (params.get('item'), params.get('total')) + if not (0 < (attr_2 - 1)): + ctx.call_remote_async(operator_name = 'user', function_name = 'drain_stock_step_3', key = ctx.key, params = ({'item': item, 'total': total}, None, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'user', 'drain_stock_step_5', ctx.key, {'item': item, 'total': total}) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (-1, reply_to)) + +@user_operator.register +async def drain_stock_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (item, total) = (params.get('item'), params.get('total')) + total += 1 + ctx.call_remote_async(operator_name = 'user', function_name = 'drain_stock_step_2', key = ctx.key, params = ({'item': item, 'total': total}, None, reply_to)) + + +@user_operator.register +async def discounted_sum(ctx: StatefulFunction, items: list[str], threshold: int, reply_to: list = None) -> int: + if not items: + return send_reply(ctx, reply_to, 0) + attr_1 = items[0] + reply_to = push_continuation(ctx, reply_to, 'user', 'discounted_sum_step_2', ctx.key, {'items': items, 'threshold': threshold}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = attr_1, params = (reply_to,)) + +@user_operator.register +async def discounted_sum_step_2(ctx: StatefulFunction, func_context, price = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (items, threshold) = (params.get('items'), params.get('threshold')) + reply_to = push_continuation(ctx, reply_to, 'user', 'discounted_sum_step_3', ctx.key, {'price': price, 'threshold': threshold}) + ctx.call_remote_async(operator_name = 'user', function_name = 'discounted_sum', key = ctx.key, params = (items[1:], threshold, reply_to)) + +@user_operator.register +async def discounted_sum_step_3(ctx: StatefulFunction, func_context, rest = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (price, threshold) = (params.get('price'), params.get('threshold')) + if price > threshold: + return send_reply(ctx, reply_to, rest + int(price * 0.9)) + return send_reply(ctx, reply_to, rest + price) + + + +@user_operator.register +async def bulk_purchase_with_tiers(ctx: StatefulFunction, cart: list[str], quantities: list[int], reply_to: list = None) -> str: + total_cost = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'quantities': quantities, 'total_cost': total_cost}, None, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, quantities, total_cost) = (params.get('__loop_index_1'), params.get('cart'), params.get('quantities'), params.get('total_cost')) + if __loop_index_1 >= len(cart): + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'quantities': quantities, 'total_cost': total_cost}, None, reply_to)) + else: + index = __loop_index_1 + __loop_index_1 += 1 + item = cart[index] + requested_amount = quantities[index] + reply_to = push_continuation(ctx, reply_to, 'user', 'bulk_purchase_with_tiers_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, quantities, total_cost) = (params.get('__loop_index_1'), params.get('cart'), params.get('quantities'), params.get('total_cost')) + __state__['balance'] -= total_cost + ctx.put(__state__) + return send_reply(ctx, reply_to, "Bulk purchase complete. Remaining balance: " + str(__state__['balance'])) + +@user_operator.register +async def bulk_purchase_with_tiers_step_4(ctx: StatefulFunction, func_context, attr_7 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + + if attr_7 >= requested_amount: + current_item_cost = 0 + __loop_index_2 = 1 + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}, None, reply_to)) + else: + logging.warning(f"Skipping {item} due to low stock.") + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'quantities': quantities, 'total_cost': total_cost}, None, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + if __loop_index_2 >= requested_amount + 1: + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}, None, reply_to)) + else: + unit = __loop_index_2 + __loop_index_2 += 1 + if unit > 50: + reply_to = push_continuation(ctx, reply_to, 'user', 'bulk_purchase_with_tiers_step_8', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + elif unit > 10: + reply_to = push_continuation(ctx, reply_to, 'user', 'bulk_purchase_with_tiers_step_9', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + else: + reply_to = push_continuation(ctx, reply_to, 'user', 'bulk_purchase_with_tiers_step_10', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + + if (total_cost + current_item_cost) > __state__['balance']: + raise NotEnoughBalance("Cannot afford the entire cart.") + reply_to = push_continuation(ctx, reply_to, 'user', 'bulk_purchase_with_tiers_step_7', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (-requested_amount, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + total_cost += current_item_cost + + for _ in range(requested_amount): + __state__['myitems'].append(item) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'quantities': quantities, 'total_cost': total_cost}, None, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_8(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + current_item_cost += int(attr_1 * 0.8) + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}, None, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_9(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + current_item_cost += int(attr_2 * 0.9) + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}, None, reply_to)) + +@user_operator.register +async def bulk_purchase_with_tiers_step_10(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, cart, current_item_cost, item, quantities, requested_amount, total_cost) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('cart'), params.get('current_item_cost'), params.get('item'), params.get('quantities'), params.get('requested_amount'), params.get('total_cost')) + current_item_cost += attr_3 + ctx.call_remote_async(operator_name = 'user', function_name = 'bulk_purchase_with_tiers_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, 'cart': cart, 'current_item_cost': current_item_cost, 'item': item, 'quantities': quantities, 'requested_amount': requested_amount, 'total_cost': total_cost}, None, reply_to)) + + +@user_operator.register +async def inventory_value(ctx: StatefulFunction, reply_to: list = None) -> int: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'inventory_value_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + +@user_operator.register +async def inventory_value_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + if __loop_index_1 >= len(__state__['myitems']): + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'inventory_value_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + else: + item = __state__['myitems'][__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'inventory_value_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'item': item}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def inventory_value_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + return send_reply(ctx, reply_to, sum(_comp_result_1)) + +@user_operator.register +async def inventory_value_step_4(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, item) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('item')) + if attr_3 > 20: + reply_to = push_continuation(ctx, reply_to, 'user', 'inventory_value_step_5', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'user', function_name = 'inventory_value_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + +@user_operator.register +async def inventory_value_step_5(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'user', function_name = 'inventory_value_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + + +@user_operator.register +async def my_item_prices(ctx: StatefulFunction, reply_to: list = None) -> list[int]: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'my_item_prices_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + +@user_operator.register +async def my_item_prices_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + if __loop_index_1 >= len(__state__['myitems']): + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'my_item_prices_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + else: + item = __state__['myitems'][__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'my_item_prices_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def my_item_prices_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + return send_reply(ctx, reply_to, _comp_result_1) + +@user_operator.register +async def my_item_prices_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'user', function_name = 'my_item_prices_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + + +@user_operator.register +async def ret_tuple(ctx: StatefulFunction, item: str, reply_to: list = None) -> tuple[int, int]: + reply_to = push_continuation(ctx, reply_to, 'user', 'ret_tuple_step_2', ctx.key, {'item': item}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def ret_tuple_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (item,) = (params.get('item'),) + reply_to = push_continuation(ctx, reply_to, 'user', 'ret_tuple_step_3', ctx.key, {'attr_1': attr_1}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + +@user_operator.register +async def ret_tuple_step_3(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (attr_1,) = (params.get('attr_1'),) + return send_reply(ctx, reply_to, (attr_1, attr_2)) + + +@user_operator.register +async def ret_dict(ctx: StatefulFunction, item: str, reply_to: list = None) -> dict[str, int]: + reply_to = push_continuation(ctx, reply_to, 'user', 'ret_dict_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'user', function_name = 'ret_tuple', key = ctx.key, params = (item, reply_to)) + +@user_operator.register +async def ret_dict_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + price, stock = attr_1 + return send_reply(ctx, reply_to, {"price": price, "stock": stock}) + + +@user_operator.register +async def fire_and_forget(ctx: StatefulFunction, item: str, reply_to: list = None) -> None: + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (1, [{'sink': True}])) + + +@user_operator.register +async def demo(ctx: StatefulFunction, reply_to: list = None) -> str: + __state__ = ctx.get() or {} + for item in __state__['myitems']: + for _ in range(100): + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'helper', key = ctx.key, params = (item, [{'sink': True}])) + ctx.put(__state__) + return send_reply(ctx, reply_to, "demo complete") + + +@user_operator.register +async def helper(ctx: StatefulFunction, item: str, reply_to: list = None) -> int: + reply_to = push_continuation(ctx, reply_to, 'user', 'helper_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (1, reply_to)) + +@user_operator.register +async def helper_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + return send_reply(ctx, reply_to, 1) + + +@user_operator.register +async def demo2(ctx: StatefulFunction, item: Optional[str] = None, reply_to: list = None) -> str: + if item is None: + return send_reply(ctx, reply_to, "No item provided") + reply_to = push_continuation(ctx, reply_to, 'user', 'demo2_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (1, reply_to)) + +@user_operator.register +async def demo2_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + return send_reply(ctx, reply_to, "demo complete") + + +@user_operator.register +async def recursion_test(ctx: StatefulFunction, items: list[str], reply_to: list = None) -> int: + if not items: + return send_reply(ctx, reply_to, 0) + attr_1 = items[0] + reply_to = push_continuation(ctx, reply_to, 'user', 'recursion_test_step_2', ctx.key, {'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = attr_1, params = (reply_to,)) + +@user_operator.register +async def recursion_test_step_2(ctx: StatefulFunction, func_context, attr_2 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (items,) = (params.get('items'),) + reply_to = push_continuation(ctx, reply_to, 'user', 'recursion_test_step_3', ctx.key, {'attr_2': attr_2}) + ctx.call_remote_async(operator_name = 'user', function_name = 'recursion_test', key = ctx.key, params = (items[1:], reply_to)) + +@user_operator.register +async def recursion_test_step_3(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (attr_2,) = (params.get('attr_2'),) + return send_reply(ctx, reply_to, attr_2 + attr_3) + + +@user_operator.register +async def comprehensions(ctx: StatefulFunction, items: list[str], reply_to: list = None) -> dict[str, int]: + _comp_result_1 = {} + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'comprehensions_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + +@user_operator.register +async def comprehensions_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + if __loop_index_1 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'comprehensions_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + else: + item = items[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'comprehensions_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'item': item, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + +@user_operator.register +async def comprehensions_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + return send_reply(ctx, reply_to, _comp_result_1) + +@user_operator.register +async def comprehensions_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, item, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('item'), params.get('items')) + _comp_result_1[item] = attr_1 + ctx.call_remote_async(operator_name = 'user', function_name = 'comprehensions_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + + +@user_operator.register +async def type_test(ctx: StatefulFunction, hard: list[list[dict[str, int]]], easy: list[list[str]], reply_to: list = None) -> str: + temp = easy[0][0] + reply_to = push_continuation(ctx, reply_to, 'user', 'type_test_step_2', ctx.key, {'hard': hard}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = temp, params = (reply_to,)) + +@user_operator.register +async def type_test_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (hard,) = (params.get('hard'),) + attr_2 = hard[0][0] + attr_3 = attr_2.keys() + attr_4 = list(attr_3)[0] + reply_to = push_continuation(ctx, reply_to, 'user', 'type_test_step_3', ctx.key, {}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = attr_4, params = (reply_to,)) + +@user_operator.register +async def type_test_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + temp4 = __state__['myitems'][0] + reply_to = push_continuation(ctx, reply_to, 'user', 'type_test_step_4', ctx.key, {}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = temp4, params = (reply_to,)) + +@user_operator.register +async def type_test_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + attr_7 = __state__['myitems'][0] + reply_to = push_continuation(ctx, reply_to, 'user', 'type_test_step_5', ctx.key, {}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = attr_7, params = (reply_to,)) + +@user_operator.register +async def type_test_step_5(ctx: StatefulFunction, func_context, stock_val = None, reply_to: list = None): + __state__ = ctx.get() or {} + lst = [__state__['myitems'][0], __state__['myitems'][1]] + attr_9 = lst[0] + reply_to = push_continuation(ctx, reply_to, 'user', 'type_test_step_6', ctx.key, {}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = attr_9, params = (reply_to,)) + +@user_operator.register +async def type_test_step_6(ctx: StatefulFunction, func_context, stock = None, reply_to: list = None): + return send_reply(ctx, reply_to, "hello") + + +@user_operator.register +async def process_cart_with_limits(ctx: StatefulFunction, cart: list[str], max_spend: int, reply_to: list = None) -> dict: + purchased = {} + total_spent = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}, None, reply_to)) + +@user_operator.register +async def process_cart_with_limits_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, max_spend, purchased, total_spent) = (params.get('__loop_index_1'), params.get('cart'), params.get('max_spend'), params.get('purchased'), params.get('total_spent')) + if __loop_index_1 >= len(cart): + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}, None, reply_to)) + else: + item = cart[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'process_cart_with_limits_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def process_cart_with_limits_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, max_spend, purchased, total_spent) = (params.get('__loop_index_1'), params.get('cart'), params.get('max_spend'), params.get('purchased'), params.get('total_spent')) + return send_reply(ctx, reply_to, purchased) + +@user_operator.register +async def process_cart_with_limits_step_4(ctx: StatefulFunction, func_context, price = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, max_spend, purchased, total_spent) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('max_spend'), params.get('purchased'), params.get('total_spent')) + + if price > __state__['balance']: + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}, None, reply_to)) + else: + + if total_spent >= max_spend: + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}, None, reply_to)) + else: + units_bought = 0 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}, None, reply_to)) + +@user_operator.register +async def process_cart_with_limits_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, max_spend, price, purchased, total_spent, units_bought) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('max_spend'), params.get('price'), params.get('purchased'), params.get('total_spent'), params.get('units_bought')) + reply_to = push_continuation(ctx, reply_to, 'user', 'process_cart_with_limits_step_7', ctx.key, {'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + +@user_operator.register +async def process_cart_with_limits_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, max_spend, price, purchased, total_spent, units_bought) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('max_spend'), params.get('price'), params.get('purchased'), params.get('total_spent'), params.get('units_bought')) + + if units_bought > 0: + purchased[item] = units_bought + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'max_spend': max_spend, 'purchased': purchased, 'total_spent': total_spent}, None, reply_to)) + +@user_operator.register +async def process_cart_with_limits_step_7(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, max_spend, price, purchased, total_spent, units_bought) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('max_spend'), params.get('price'), params.get('purchased'), params.get('total_spent'), params.get('units_bought')) + if not (0 < attr_3): + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}, None, reply_to)) + else: + if total_spent + price > max_spend: + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}, None, reply_to)) + else: + if price > __state__['balance']: + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}, None, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'user', 'process_cart_with_limits_step_8', ctx.key, {'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (-1, reply_to)) + +@user_operator.register +async def process_cart_with_limits_step_8(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, cart, item, max_spend, price, purchased, total_spent, units_bought) = (params.get('__loop_index_1'), params.get('cart'), params.get('item'), params.get('max_spend'), params.get('price'), params.get('purchased'), params.get('total_spent'), params.get('units_bought')) + __state__['balance'] -= price + total_spent += price + units_bought += 1 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'process_cart_with_limits_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'cart': cart, 'item': item, 'max_spend': max_spend, 'price': price, 'purchased': purchased, 'total_spent': total_spent, 'units_bought': units_bought}, None, reply_to)) + + +@user_operator.register +async def transfer_balance(ctx: StatefulFunction, recipient: 'User', amount: int, reply_to: list = None) -> bool: + __state__ = ctx.get() or {} + if __state__['balance'] < amount: + raise NotEnoughBalance("Insufficient balance for transfer.") + __state__['balance'] -= amount + reply_to = push_continuation(ctx, reply_to, 'user', 'transfer_balance_step_2', ctx.key, {}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'add_balance', key = recipient, params = (amount, reply_to)) + +@user_operator.register +async def transfer_balance_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + return send_reply(ctx, reply_to, True) + + +@user_operator.register +async def multi_restock(ctx: StatefulFunction, items: list[str], amounts: list[int], reply_to: list = None) -> int: + total_added = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'multi_restock_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'amounts': amounts, 'items': items, 'total_added': total_added}, None, reply_to)) + +@user_operator.register +async def multi_restock_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, amounts, items, total_added) = (params.get('__loop_index_1'), params.get('amounts'), params.get('items'), params.get('total_added')) + if __loop_index_1 >= min(len(items), len(amounts)): + ctx.call_remote_async(operator_name = 'user', function_name = 'multi_restock_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'amounts': amounts, 'items': items, 'total_added': total_added}, None, reply_to)) + else: + item = items[__loop_index_1] + amount = amounts[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'multi_restock_step_4', ctx.key, {'__loop_index_1': __loop_index_1, 'amount': amount, 'amounts': amounts, 'items': items, 'total_added': total_added}) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (amount, reply_to)) + +@user_operator.register +async def multi_restock_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, amounts, items, total_added) = (params.get('__loop_index_1'), params.get('amounts'), params.get('items'), params.get('total_added')) + return send_reply(ctx, reply_to, total_added) + +@user_operator.register +async def multi_restock_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, amount, amounts, items, total_added) = (params.get('__loop_index_1'), params.get('amount'), params.get('amounts'), params.get('items'), params.get('total_added')) + total_added += amount + ctx.call_remote_async(operator_name = 'user', function_name = 'multi_restock_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'amounts': amounts, 'items': items, 'total_added': total_added}, None, reply_to)) + + +@user_operator.register +async def most_valuable_item_price(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + if not __state__['myitems']: + ctx.put(__state__) + return send_reply(ctx, reply_to, 0) + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'most_valuable_item_price_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + +@user_operator.register +async def most_valuable_item_price_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + if __loop_index_1 >= len(__state__['myitems']): + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'most_valuable_item_price_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + else: + item = __state__['myitems'][__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'most_valuable_item_price_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def most_valuable_item_price_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + return send_reply(ctx, reply_to, max(_comp_result_1)) + +@user_operator.register +async def most_valuable_item_price_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1) = (params.get('__loop_index_1'), params.get('_comp_result_1')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'user', function_name = 'most_valuable_item_price_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1}, None, reply_to)) + + +@user_operator.register +async def can_afford_cart(ctx: StatefulFunction, items: list[str], reply_to: list = None) -> bool: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'can_afford_cart_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + +@user_operator.register +async def can_afford_cart_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + if __loop_index_1 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'can_afford_cart_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + else: + item = items[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'can_afford_cart_step_4', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def can_afford_cart_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + total = sum(_comp_result_1) + ctx.put(__state__) + return send_reply(ctx, reply_to, __state__['balance'] >= total) + +@user_operator.register +async def can_afford_cart_step_4(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'user', function_name = 'can_afford_cart_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + + +@user_operator.register +async def group_items_by_price_bucket(ctx: StatefulFunction, items: list[str], reply_to: list = None) -> dict: + _comp_result_1 = [] + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + if __loop_index_1 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + else: + item = items[__loop_index_1] + __loop_index_1 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_12', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'item': item, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def group_items_by_price_bucket_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + _comp_result_2 = [] + __loop_index_2 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('items')) + if __loop_index_2 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_5', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'items': items}, None, reply_to)) + else: + item = items[__loop_index_2] + __loop_index_2 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_10', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'item': item, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def group_items_by_price_bucket_step_5(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('items')) + _comp_result_3 = [] + __loop_index_3 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_6(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('items')) + if __loop_index_3 >= len(items): + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_7', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'items': items}, None, reply_to)) + else: + item = items[__loop_index_3] + __loop_index_3 += 1 + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_8', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'item': item, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def group_items_by_price_bucket_step_7(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('items')) + return send_reply(ctx, reply_to, { + 'cheap': _comp_result_1, + 'mid': _comp_result_2, + 'expensive': _comp_result_3, + }) + +@user_operator.register +async def group_items_by_price_bucket_step_8(ctx: StatefulFunction, func_context, attr_9 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, item, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('item'), params.get('items')) + if attr_9 > 100: + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_9', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_9(ctx: StatefulFunction, func_context, attr_7 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, __loop_index_3, _comp_result_1, _comp_result_2, _comp_result_3, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('__loop_index_3'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('_comp_result_3'), params.get('items')) + _comp_result_3.append(attr_7) + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_6', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '__loop_index_3': __loop_index_3, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, '_comp_result_3': _comp_result_3, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_10(ctx: StatefulFunction, func_context, attr_6 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, item, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('item'), params.get('items')) + if 20 <= attr_6 <= 100: + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_11', ctx.key, {'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_11(ctx: StatefulFunction, func_context, attr_4 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, __loop_index_2, _comp_result_1, _comp_result_2, items) = (params.get('__loop_index_1'), params.get('__loop_index_2'), params.get('_comp_result_1'), params.get('_comp_result_2'), params.get('items')) + _comp_result_2.append(attr_4) + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_4', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '__loop_index_2': __loop_index_2, '_comp_result_1': _comp_result_1, '_comp_result_2': _comp_result_2, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_12(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, item, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('item'), params.get('items')) + if attr_3 < 20: + reply_to = push_continuation(ctx, reply_to, 'user', 'group_items_by_price_bucket_step_13', ctx.key, {'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + else: + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + +@user_operator.register +async def group_items_by_price_bucket_step_13(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, _comp_result_1, items) = (params.get('__loop_index_1'), params.get('_comp_result_1'), params.get('items')) + _comp_result_1.append(attr_1) + ctx.call_remote_async(operator_name = 'user', function_name = 'group_items_by_price_bucket_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, '_comp_result_1': _comp_result_1, 'items': items}, None, reply_to)) + + +@user_operator.register +async def is_in_stock(ctx: StatefulFunction, item: str, reply_to: list = None) -> bool: + _sc_1 = item is not None + if _sc_1: + reply_to = push_continuation(ctx, reply_to, 'user', 'is_in_stock_step_2', ctx.key, {}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_stock', key = item, params = (reply_to,)) + else: + return send_reply(ctx, reply_to, _sc_1) + +@user_operator.register +async def is_in_stock_step_2(ctx: StatefulFunction, func_context, attr_1 = None, reply_to: list = None): + _sc_1 = attr_1 > 0 + return send_reply(ctx, reply_to, _sc_1) + + + + +@user_operator.register +async def get_discounted_price(ctx: StatefulFunction, item: str, coupon: str, reply_to: list = None) -> int: + _gather_id = init_gather_barrier(ctx, 2, {}, reply_to) + _g_reply_0 = [{'op_name': 'user', 'fun': 'get_discounted_price_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 0}}] + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (_g_reply_0,)) + _g_reply_1 = [{'op_name': 'user', 'fun': 'get_discounted_price_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 1}}] + ctx.call_remote_async(operator_name = 'coupon', function_name = 'get_discount', key = coupon, params = (_g_reply_1,)) + +@user_operator.register +async def get_discounted_price_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + reply_to = parent_reply_to + price, discount = _g_results + discounted_price = price - discount + return send_reply(ctx, reply_to, max(discounted_price, 0)) + + +@user_operator.register +async def buy_with_coupon(ctx: StatefulFunction, item: str, coupon: Optional[str], reply_to: list = None) -> bool: + if coupon is None: + ctx.call_remote_async(operator_name = 'user', function_name = 'buy_item', key = ctx.key, params = (1, item, reply_to)) + else: + reply_to = push_continuation(ctx, reply_to, 'user', 'buy_with_coupon_step_2', ctx.key, {'item': item}) + ctx.call_remote_async(operator_name = 'user', function_name = 'get_discounted_price', key = ctx.key, params = (item, coupon, reply_to)) + +@user_operator.register +async def buy_with_coupon_step_2(ctx: StatefulFunction, func_context, discounted_price = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (item,) = (params.get('item'),) + + if __state__['balance'] < discounted_price: + raise NotEnoughBalance("Not enough balance to buy the item with coupon.") + reply_to = push_continuation(ctx, reply_to, 'user', 'buy_with_coupon_step_3', ctx.key, {'discounted_price': discounted_price, 'item': item}) + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'is_in_stock', key = ctx.key, params = (item, reply_to)) + +@user_operator.register +async def buy_with_coupon_step_3(ctx: StatefulFunction, func_context, attr_3 = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (discounted_price, item) = (params.get('discounted_price'), params.get('item')) + if not attr_3: + raise OutOfStock("Item is out of stock.") + reply_to = push_continuation(ctx, reply_to, 'user', 'buy_with_coupon_step_4', ctx.key, {'discounted_price': discounted_price, 'item': item}) + ctx.call_remote_async(operator_name = 'item', function_name = 'update_stock', key = item, params = (-1, reply_to)) + +@user_operator.register +async def buy_with_coupon_step_4(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + __state__ = ctx.get() or {} + params = resolve_context(ctx, func_context) + (discounted_price, item) = (params.get('discounted_price'), params.get('item')) + __state__['balance'] -= discounted_price + __state__['myitems'].append(item) + ctx.put(__state__) + return send_reply(ctx, reply_to, True) + + + +@user_operator.register +async def gather_in_loop(ctx: StatefulFunction, items: list[str], coupons: list[str], reply_to: list = None) -> int: + total = 0 + __loop_index_1 = 0 + ctx.call_remote_async(operator_name = 'user', function_name = 'gather_in_loop_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupons': coupons, 'items': items, 'total': total}, None, reply_to)) + +@user_operator.register +async def gather_in_loop_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, coupons, items, total) = (params.get('__loop_index_1'), params.get('coupons'), params.get('items'), params.get('total')) + if __loop_index_1 >= min(len(items), len(coupons)): + ctx.call_remote_async(operator_name = 'user', function_name = 'gather_in_loop_step_3', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupons': coupons, 'items': items, 'total': total}, None, reply_to)) + else: + item = items[__loop_index_1] + coupon = coupons[__loop_index_1] + __loop_index_1 += 1 + _gather_id = init_gather_barrier(ctx, 2, {'__loop_index_1': __loop_index_1, 'coupons': coupons, 'items': items, 'total': total}, reply_to) + _g_reply_0 = [{'op_name': 'user', 'fun': 'gather_in_loop_step_4', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 0}}] + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (_g_reply_0,)) + _g_reply_1 = [{'op_name': 'user', 'fun': 'gather_in_loop_step_4', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 1}}] + ctx.call_remote_async(operator_name = 'coupon', function_name = 'get_discount', key = coupon, params = (_g_reply_1,)) + +@user_operator.register +async def gather_in_loop_step_3(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (__loop_index_1, coupons, items, total) = (params.get('__loop_index_1'), params.get('coupons'), params.get('items'), params.get('total')) + return send_reply(ctx, reply_to, total) + +@user_operator.register +async def gather_in_loop_step_4(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + (__loop_index_1, coupons, items, total) = (saved.get('__loop_index_1'), saved.get('coupons'), saved.get('items'), saved.get('total')) + reply_to = parent_reply_to + price, discount = _g_results + discounted_price = price - discount + total += discounted_price + ctx.call_remote_async(operator_name = 'user', function_name = 'gather_in_loop_step_2', key = ctx.key, params = ({'__loop_index_1': __loop_index_1, 'coupons': coupons, 'items': items, 'total': total}, None, reply_to)) + + +@user_operator.register +async def inventory_value_gather(ctx: StatefulFunction, reply_to: list = None) -> int: + __state__ = ctx.get() or {} + _g_iter = list(__state__['myitems']) + _gather_id = init_gather_barrier(ctx, len(_g_iter), {}, reply_to) + if len(_g_iter) == 0: + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'user', function_name = 'inventory_value_gather_step_2', key = ctx.key, params = ({'_g_barrier': _gather_id, '_g_tag': 0}, None, None)) + else: + for (_g_tag, item) in enumerate(_g_iter): + _g_reply = [{'op_name': 'user', 'fun': 'inventory_value_gather_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': _g_tag}}] + ctx.put(__state__) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (_g_reply,)) + ctx.put(__state__) + +@user_operator.register +async def inventory_value_gather_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + reply_to = parent_reply_to + prices = _g_results + return send_reply(ctx, reply_to, sum(list(prices))) + + +@user_operator.register +async def reference_test(ctx: StatefulFunction, item: str, reply_to: list = None) -> list[int]: + list_1 = [1, 2, 3] + list_2 = list_1 + list_2.append(4) + reply_to = push_continuation(ctx, reply_to, 'user', 'reference_test_step_2', ctx.key, {'list_1': list_1, 'list_2': list_2}) + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = item, params = (reply_to,)) + +@user_operator.register +async def reference_test_step_2(ctx: StatefulFunction, func_context, placeholder_return = None, reply_to: list = None): + params = resolve_context(ctx, func_context) + (list_1, list_2) = (params.get('list_1'), params.get('list_2')) + list_2.append(5) + list_1.append(6) + return send_reply(ctx, reply_to, list_1) + + +@user_operator.register +async def price_check(ctx: StatefulFunction, a: str, b: str, coupon: str, reply_to: list = None) -> int: + _gather_id = init_gather_barrier(ctx, 3, {}, reply_to) + _g_reply_0 = [{'op_name': 'user', 'fun': 'price_check_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 0}}] + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = a, params = (_g_reply_0,)) + _g_reply_1 = [{'op_name': 'user', 'fun': 'price_check_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 1}}] + ctx.call_remote_async(operator_name = 'item', function_name = 'get_price', key = b, params = (_g_reply_1,)) + _g_reply_2 = [{'op_name': 'user', 'fun': 'price_check_step_2', 'id': ctx.key, 'context': {'_g_barrier': _gather_id, '_g_tag': 2}}] + ctx.call_remote_async(operator_name = 'coupon', function_name = 'get_discount', key = coupon, params = (_g_reply_2,)) + +@user_operator.register +async def price_check_step_2(ctx: StatefulFunction, func_context, _gather_partial = None, reply_to: list = None): + barrier_id = func_context['_g_barrier'] + _g_tag = func_context['_g_tag'] + (is_complete, _g_results, saved, parent_reply_to) = update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial) + if not is_complete: + return + reply_to = parent_reply_to + pa, pb, d = _g_results + return send_reply(ctx, reply_to, max(pa + pb - d, 0)) + diff --git a/obol/examples/original/marketplace.py b/obol/examples/original/marketplace.py new file mode 100644 index 00000000..95777d61 --- /dev/null +++ b/obol/examples/original/marketplace.py @@ -0,0 +1,929 @@ +from typing import Optional +from obol.api import entity, send_async + + +# ────────────────────────────────────────── +# Custom Exceptions (all trigger rollback) +# ────────────────────────────────────────── + +class InsufficientFunds(Exception): + pass + +class InsufficientStock(Exception): + pass + +class InvalidCoupon(Exception): + pass + +class SellerSuspended(Exception): + pass + +class OrderAlreadyFulfilled(Exception): + pass + +class WarehouseCapacityExceeded(Exception): + pass + +class ReviewAlreadySubmitted(Exception): + pass + + +# ────────────────────────────────────────── +# Entity: Product +# ────────────────────────────────────────── + +@entity +class Product: + def __init__(self, product_id: str, name: str, base_price: int, seller: 'Seller'): + self.product_id: str = product_id + self.name: str = name + self.base_price: int = base_price + self.seller: 'Seller' = seller + self.stock: int = 0 + self.total_sold: int = 0 + self.rating_sum: int = 0 + self.rating_count: int = 0 + self.tags: list[str] = [] + self.is_active: bool = True + + def __key__(self): + return self.product_id + + def get_product_id(self) -> str: + return self.product_id + + def get_price(self) -> int: + return self.base_price + + def get_stock(self) -> int: + return self.stock + + def get_seller(self) -> 'Seller': + return self.seller + + def is_available(self) -> bool: + return self.is_active & (self.stock > 0) + + def get_average_rating(self) -> int: + if self.rating_count == 0: + return 0 + return self.rating_sum // self.rating_count + + def add_stock(self, amount: int) -> bool: + if amount <= 0: + raise InsufficientStock("Stock amount must be positive.") + self.stock += amount + return True + + def deduct_stock(self, amount: int) -> bool: + if amount <= 0: + raise InsufficientStock("Amount must be positive.") + if not self.is_active: + raise InsufficientStock("Product is no longer active.") + if self.stock < amount: + raise InsufficientStock("Not enough stock for product.") + + self.stock -= amount + self.total_sold += amount + return True + + + def add_rating(self, score: int) -> int: + if (score < 0) | (score > 10): + raise ValueError("Rating must be between 0 and 10.") + self.rating_sum += score + self.rating_count += 1 + return self.get_average_rating() + + def deactivate(self) -> bool: + self.is_active = False + return True + + def add_tag(self, tag: str) -> bool: + if tag not in self.tags: + self.tags.append(tag) + return True + + def get_tags(self) -> list[str]: + return self.tags + + def get_total_sold(self) -> int: + return self.total_sold + + def get_popularity_score(self) -> int: + avg = self.get_average_rating() + return self.total_sold * 10 + avg * 50 + self.rating_count * 5 + + +# ────────────────────────────────────────── +# Entity: Seller +# ────────────────────────────────────────── + +@entity +class Seller: + def __init__(self, seller_id: str, name: str): + self.seller_id: str = seller_id + self.name: str = name + self.balance: int = 0 + self.products: list['Product'] = [] + self.total_revenue: int = 0 + self.is_suspended: bool = False + self.penalty_points: int = 0 + + def __key__(self): + return self.seller_id + + def get_seller_id(self) -> str: + return self.seller_id + + def is_active(self) -> bool: + return not self.is_suspended + + def get_balance(self) -> int: + return self.balance + + def get_revenue(self) -> int: + return self.total_revenue + + def add_product(self, product: 'Product') -> bool: + if self.is_suspended: + raise SellerSuspended("Seller is suspended and cannot add products.") + self.products.append(product) + return True + + def credit_sale(self, amount: int) -> bool: + if self.is_suspended: + raise SellerSuspended("Seller is suspended.") + self.balance += amount + self.total_revenue += amount + return True + + def debit_penalty(self, amount: int) -> bool: + self.penalty_points += 1 + self.balance -= amount + if self.penalty_points >= 5: + self.is_suspended = True + return True + + def withdraw(self, amount: int) -> bool: + if self.balance < amount: + raise InsufficientFunds("Seller does not have enough balance to withdraw.") + self.balance -= amount + return True + + def get_products(self) -> list['Product']: + return self.products + + def get_penalty_points(self) -> int: + return self.penalty_points + + def reinstate(self) -> bool: + self.is_suspended = False + self.penalty_points = 0 + return True + + +# ────────────────────────────────────────── +# Entity: Customer +# ────────────────────────────────────────── + +@entity +class Customer: + def __init__(self, customer_id: str, username: str): + self.customer_id: str = customer_id + self.username: str = username + self.balance: int = 0 + self.cart: list[str] = [] + self.order_history: list[str] = [] + self.wishlist: list[str] = [] + self.loyalty_points: int = 0 + self.reviewed_products: list[str] = [] + + def __key__(self): + return self.customer_id + + def get_order_history(self) -> list[str]: + return self.order_history + + def get_balance(self) -> int: + return self.balance + + def get_loyalty_points(self) -> int: + return self.loyalty_points + + def add_funds(self, amount: int) -> bool: + self.balance += amount + return True + + def deduct_funds(self, amount: int) -> bool: + if self.balance < amount: + raise InsufficientFunds("Customer does not have enough balance.") + self.balance -= amount + return True + + def add_to_cart(self, product_id: str) -> bool: + if product_id not in self.cart: + self.cart.append(product_id) + return True + + def remove_from_cart(self, product_id: str) -> bool: + if product_id in self.cart: + self.cart.remove(product_id) + return True + + def clear_cart(self) -> bool: + self.cart = [] + return True + + def get_cart(self) -> list[str]: + return self.cart + + def add_to_wishlist(self, product_id: str) -> bool: + if product_id not in self.wishlist: + self.wishlist.append(product_id) + return True + + def add_order(self, order_id: str) -> bool: + self.order_history.append(order_id) + return True + + def earn_loyalty_points(self, amount_spent: int) -> int: + earned = amount_spent // 100 + self.loyalty_points += earned + return earned + + def redeem_loyalty_points(self, points: int) -> int: + if self.loyalty_points < points: + points = self.loyalty_points + self.loyalty_points -= points + return points * 10 + + def has_reviewed(self, product_id: str) -> bool: + return product_id in self.reviewed_products + + def mark_reviewed(self, product_id: str) -> bool: + if product_id in self.reviewed_products: + raise ReviewAlreadySubmitted("Customer already reviewed this product.") + self.reviewed_products.append(product_id) + return True + + def get_order_count(self) -> int: + return len(self.order_history) + + def get_wishlist(self) -> list[str]: + return self.wishlist + + +# ────────────────────────────────────────── +# Entity: Coupon +# ────────────────────────────────────────── + +@entity +class Coupon: + def __init__(self, code: str, discount_percent: int, max_uses: int, min_order_value: int): + self.code: str = code + self.discount_percent: int = discount_percent + self.max_uses: int = max_uses + self.uses: int = 0 + self.min_order_value: int = min_order_value + self.is_active: bool = True + + def __key__(self): + return self.code + + def is_valid(self) -> bool: + return self.is_active & (self.uses < self.max_uses) + + def get_discount_percent(self) -> int: + return self.discount_percent + + def get_min_order_value(self) -> int: + return self.min_order_value + + def apply(self, order_value: int) -> int: + if not self.is_valid(): + raise InvalidCoupon("Coupon is expired or has reached max uses.") + if order_value < self.min_order_value: + raise InvalidCoupon("Order value too low for this coupon.") + self.uses += 1 + discount = (order_value * self.discount_percent) // 100 + return discount + + def deactivate(self) -> bool: + self.is_active = False + return True + + def get_remaining_uses(self) -> int: + return self.max_uses - self.uses + + +# ────────────────────────────────────────── +# Entity: Warehouse +# ────────────────────────────────────────── + +@entity +class Warehouse: + def __init__(self, warehouse_id: str, capacity: int): + self.warehouse_id: str = warehouse_id + self.capacity: int = capacity + self.used_capacity: int = 0 + self.product_slots: dict[str, int] = {} + self.pending_shipments: list[str] = [] + self.total_shipped: int = 0 + + def __key__(self): + return self.warehouse_id + + def get_available_capacity(self) -> int: + return self.capacity - self.used_capacity + + def get_used_capacity(self) -> int: + return self.used_capacity + + def store_product(self, product_id: str, quantity: int) -> bool: + if quantity > self.get_available_capacity(): + raise WarehouseCapacityExceeded("Not enough space in warehouse.") + current = self.product_slots.get(product_id, 0) + self.product_slots[product_id] = current + quantity + self.used_capacity += quantity + return True + + def remove_product(self, product_id: str, quantity: int) -> bool: + if quantity <= 0: + raise InsufficientStock("Quantity must be positive.") + + current = self.product_slots.get(product_id, 0) + if current < quantity: + raise InsufficientStock("Not enough of this product in warehouse.") + + new_qty = current - quantity + + if new_qty == 0: + del self.product_slots[product_id] + else: + self.product_slots[product_id] = new_qty + + self.used_capacity -= quantity + return True + + def get_product_quantity(self, product_id: str) -> int: + return self.product_slots.get(product_id, 0) + + def add_pending_shipment(self, order_id: str) -> bool: + self.pending_shipments.append(order_id) + return True + + def dispatch_shipment(self, order_id: str) -> bool: + if order_id not in self.pending_shipments: + raise OrderAlreadyFulfilled("Order not found in pending shipments.") + self.pending_shipments.remove(order_id) + self.total_shipped += 1 + return True + + def get_total_shipped(self) -> int: + return self.total_shipped + + def get_pending_count(self) -> int: + return len(self.pending_shipments) + + def calculate_fill_rate(self) -> int: + if self.capacity == 0: + return 0 + return (self.used_capacity * 100) // self.capacity + + +# ────────────────────────────────────────── +# Entity: Marketplace +# ────────────────────────────────────────── + +@entity +class Marketplace: + def __init__(self, marketplace_id: str): + self.marketplace_id: str = marketplace_id + self.registered_sellers: list[str] = [] + self.registered_customers: list[str] = [] + self.all_products: list[str] = [] + self.total_transactions: int = 0 + self.total_revenue: int = 0 + self.platform_fee_percent: int = 5 + + def __key__(self): + return self.marketplace_id + + def register_seller(self, seller_id: str) -> bool: + if seller_id not in self.registered_sellers: + self.registered_sellers.append(seller_id) + return True + + def register_customer(self, customer_id: str) -> bool: + if customer_id not in self.registered_customers: + self.registered_customers.append(customer_id) + return True + + def list_product(self, product_id: str) -> bool: + if product_id not in self.all_products: + self.all_products.append(product_id) + return True + + def record_transaction(self, amount: int) -> bool: + fee = (amount * self.platform_fee_percent) // 100 + self.total_revenue += fee + self.total_transactions += 1 + return True + + def get_stats(self) -> dict[str, int]: + return { + "sellers": len(self.registered_sellers), + "customers": len(self.registered_customers), + "products": len(self.all_products), + "transactions": self.total_transactions, + "revenue": self.total_revenue, + } + + def get_platform_fee(self) -> int: + return self.platform_fee_percent + + def get_total_revenue(self) -> int: + return self.total_revenue + + def get_product_count(self) -> int: + return len(self.all_products) + + # ── Complex orchestration methods ────── + + def purchase( + self, + customer: Customer, + product: Product, + warehouse: Warehouse, + quantity: int, + coupon_code: Optional[str], + coupon: Optional[Coupon], + use_loyalty: bool, + ) -> str: + + if quantity <= 0: + raise ValueError("Quantity must be positive.") + + seller = product.get_seller() + + if not product.is_available(): + raise InsufficientStock("Product not available.") + + if not seller.is_active(): + raise SellerSuspended("Seller is suspended.") + + if warehouse.get_product_quantity(product.get_product_id()) < quantity: + raise InsufficientStock("Warehouse does not have enough stock.") + + base_cost = product.get_price() * quantity + discount = 0 + + if coupon is not None: + if coupon_code is not None: + if coupon.code != coupon_code: + raise InvalidCoupon("Coupon code mismatch.") + discount = coupon.apply(base_cost) + + loyalty_discount = 0 + if use_loyalty: + max_loyalty_discount = int(base_cost * 0.3) + redeemable_points = customer.get_loyalty_points() // 2 + potential_discount = redeemable_points * 10 + actual_discount = min(max_loyalty_discount, potential_discount) + points_to_use = actual_discount // 10 + loyalty_discount = customer.redeem_loyalty_points(points_to_use) + + final_cost = base_cost - discount - loyalty_discount + if final_cost < 0: + final_cost = 0 + + customer.deduct_funds(final_cost) + + product.deduct_stock(quantity) + warehouse.remove_product(product.get_product_id(), quantity) + + platform_fee = (final_cost * self.platform_fee_percent) // 100 + seller_cut = final_cost - platform_fee + seller.credit_sale(seller_cut) + + if final_cost > 0: + customer.earn_loyalty_points(final_cost) + + self.record_transaction(final_cost) + + order_id = ( + seller.get_seller_id() + + "_" + + product.get_product_id() + + "_" + + str(self.total_transactions) + ) + + customer.add_order(order_id) + warehouse.add_pending_shipment(order_id) + + return order_id + + + def batch_restock( + self, + products: list[Product], + quantities: list[int], + warehouse: Warehouse, + ) -> str: + restocked = 0 + skipped = 0 + + for i in range(len(products)): + p = products[i] + qty = quantities[i] + available_space = warehouse.get_available_capacity() + + if available_space >= qty: + p.add_stock(qty) + warehouse.store_product(p.get_product_id(), qty) + restocked += 1 + else: + skipped += 1 + + return "Restocked: " + str(restocked) + ", Skipped: " + str(skipped) + + def compute_cart_total( + self, + customer: Customer, + products: list[Product], + quantities: list[int], + ) -> int: + + total = 0 + + for i in range(len(products)): + p = products[i] + qty = quantities[i] + price = p.get_price() + + if qty <= 5: + item_total = qty * price + elif qty <= 20: + item_total = 5 * price + int((qty - 5) * price * 0.9) + else: + item_total = ( + 5 * price + + int(15 * price * 0.9) + + int((qty - 20) * price * 0.8) + ) + + total += item_total + + return total + + def submit_review( + self, + customer: Customer, + product: Product, + score: int, + ) -> int: + + product_id = product.get_product_id() + has_purchased = any([product_id in order_id for order_id in customer.get_order_history()]) + if not has_purchased: + raise Exception("Customer cannot review a product they haven't purchased.") + + customer.mark_reviewed(product.get_product_id()) + new_avg = product.add_rating(score) + + send_async(customer.earn_loyalty_points(20)) + return new_avg + + def get_top_product_scores(self, products: list[Product]) -> list[int]: + scores = [p.get_popularity_score() for p in products] + return scores + + def get_affordable_products( + self, products: list[Product], budget: int + ) -> list[Product]: + affordable = [p for p in products if (p.get_price() <= budget) & p.is_available()] + return affordable + + def total_wishlist_value(self, customer: Customer, products: list[Product]) -> int: + wishlist = customer.get_wishlist() + total = sum([p.get_price() for p in products if p.get_product_id() in wishlist]) + return total + + def suspend_seller_and_deactivate_products( + self, + seller: Seller, + products: list[Product], + ) -> str: + seller.debit_penalty(500) + deactivated = 0 + for p in products: + if (p.get_seller() == seller): + p.deactivate() + deactivated += 1 + return "Deactivated " + str(deactivated) + " products for seller " + seller.get_seller_id() + + def restock_and_report( + self, + seller: Seller, + products: list[Product], + quantities: list[int], + warehouse: Warehouse, + ) -> dict[str, int]: + total_units = 0 + total_fee = 0 + + for i in range(len(products)): + p = products[i] + qty = quantities[i] + fee = qty * 2 + + if warehouse.get_available_capacity() < qty: + raise WarehouseCapacityExceeded("Cannot restock: warehouse is full.") + + seller.withdraw(fee) + p.add_stock(qty) + warehouse.store_product(p.get_product_id(), qty) + total_units += qty + total_fee += fee + + return {"units_restocked": total_units, "fees_charged": total_fee} + + def process_bulk_orders( + self, + customers: list[Customer], + product: Product, + quantity_each: int, + warehouse: Warehouse, + ) -> str: + + if quantity_each <= 0: + raise ValueError("Quantity must be positive.") + + seller = product.get_seller() + + success_count = 0 + skip_count = 0 + unit_price = product.get_price() + cost = unit_price * quantity_each + + for customer in customers: + + if ( + (customer.get_balance() >= cost) + & (product.get_stock() >= quantity_each) + & (product.is_available()) + & (seller.is_active()) + & (warehouse.get_product_quantity(product.get_product_id()) >= quantity_each) + ): + customer.deduct_funds(cost) + product.deduct_stock(quantity_each) + warehouse.remove_product(product.get_product_id(), quantity_each) + + platform_fee = (cost * self.platform_fee_percent) // 100 + seller.credit_sale(cost - platform_fee) + + customer.earn_loyalty_points(cost) + self.record_transaction(cost) + + success_count += 1 + else: + skip_count += 1 + + return f"Bulk orders done. Success: {success_count}, Skipped: {skip_count}" + + def warehouse_health_check(self, warehouses: list[Warehouse]) -> list[int]: + return [w.calculate_fill_rate() for w in warehouses] + + def find_overstocked_warehouses( + self, warehouses: list[Warehouse], threshold: int + ) -> list[Warehouse]: + return [w for w in warehouses if w.calculate_fill_rate() > threshold] + + def seller_revenue_summary(self, sellers: list[Seller]) -> dict[Seller, int]: + return {s: s.get_revenue() for s in sellers} + + def rank_products_by_popularity(self, products: list[Product]) -> list[int]: + scores = [p.get_popularity_score() for p in products] + scores.sort(reverse=True) + return scores + + def total_platform_earnings_from_sellers(self, sellers: list[Seller]) -> int: + total = sum( + [(s.get_revenue() * self.platform_fee_percent) // 100 + for s in sellers] + ) + return total + + + def multi_product_availability_check( + self, products: list[Product], quantities: list[int] + ) -> bool: + + for i in range(len(products)): + p = products[i] + if ( + (p.get_stock() < quantities[i]) + | (not p.is_available()) + | (not p.get_seller().is_active()) + ): + return False + + return True + + def compute_order_breakdown( + self, + products: list[Product], + quantities: list[int], + coupon: Coupon, + ) -> tuple[int, int, int]: + subtotal = 0 + for i in range(len(products)): + subtotal += products[i].get_price() * quantities[i] + + discount = coupon.apply(subtotal) + final = subtotal - discount + + return (subtotal, discount, final) + + def loyalty_cashback_campaign( + self, customers: list[Customer], products: list[Product] + ) -> int: + active_count = 0 + for p in products: + if p.is_available(): + active_count += 1 + + total_points_granted = 0 + for c in customers: + c.earn_loyalty_points(active_count * 100) + total_points_granted += active_count + + return total_points_granted + + def get_seller_product_prices( + self, seller: Seller, products: list[Product] + ) -> list[int]: + return [ + p.get_price() + for p in products + if p.get_seller() == seller + ] + + def cross_entity_stats( + self, + sellers: list[Seller], + customers: list[Customer], + products: list[Product], + warehouses: list[Warehouse], + ) -> str: + total_seller_balance = sum([s.get_balance() for s in sellers]) + total_customer_balance = sum([c.get_balance() for c in customers]) + active_products = [p for p in products if p.is_available()] + avg_stock = sum([p.get_stock() for p in active_products]) + warehouse_fill_rates = [w.calculate_fill_rate() for w in warehouses] + avg_fill = sum(warehouse_fill_rates) // len(warehouse_fill_rates) if warehouse_fill_rates else 0 + + return ( + "Sellers balance: " + str(total_seller_balance) + + " | Customers balance: " + str(total_customer_balance) + + " | Active products: " + str(len(active_products)) + + " | Total stock: " + str(avg_stock) + + " | Avg warehouse fill: " + str(avg_fill) + "%" + ) + + def fire_restock_notifications( + self, products: list[Product], threshold: int + ) -> None: + for p in products: + if p.get_stock() < threshold: + send_async(p.add_tag("low_stock")) + + def checkout_with_loyalty_and_coupon( + self, + customer: Customer, + products: list[Product], + quantities: list[int], + coupon: Coupon, + warehouse: Warehouse, + ) -> str: + subtotal = self.compute_cart_total(customer, products, quantities) + + discount = coupon.apply(subtotal) + after_coupon = subtotal - discount + + points_to_redeem = customer.get_loyalty_points() // 2 + cashback = customer.redeem_loyalty_points(points_to_redeem) + final = after_coupon - cashback + if final < 0: + final = 0 + + if customer.get_balance() < final: + raise InsufficientFunds("Customer cannot afford cart after discounts.") + + for i in range(len(products)): + p = products[i] + qty = quantities[i] + p.deduct_stock(qty) + warehouse.remove_product(p.get_product_id(), qty) + + customer.deduct_funds(final) + customer.earn_loyalty_points(final) + self.record_transaction(final) + + return "Checkout complete. Paid: " + str(final) + ", Loyalty earned: " + str(final // 100) + + def recursive_price_sum(self, products: list[Product]) -> int: + if not products: + return 0 + head_price = products[0].get_price() + rest_sum = self.recursive_price_sum(products[1:]) + return head_price + rest_sum + + def tag_popular_products( + self, products: list[Product], score_threshold: int + ) -> int: + tagged = 0 + for p in products: + score = p.get_popularity_score() + if score >= score_threshold: + send_async(p.add_tag("trending")) + tagged += 1 + return tagged + + def get_customer_cart_value( + self, customer: Customer, products: list[Product] + ) -> int: + cart = customer.get_cart() + total = sum([p.get_price() for p in products if p.get_product_id() in cart]) + return total + + def rebalance_warehouses( + self, + source: Warehouse, + destination: Warehouse, + product_id: str, + transfer_qty: int, + ) -> str: + available_in_source = source.get_product_quantity(product_id) + if available_in_source < transfer_qty: + raise InsufficientStock("Source warehouse does not have enough stock.") + + if destination.get_available_capacity() < transfer_qty: + raise WarehouseCapacityExceeded("Destination warehouse cannot fit the transfer.") + + source.remove_product(product_id, transfer_qty) + destination.store_product(product_id, transfer_qty) + + return "Transferred " + str(transfer_qty) + " units of " + product_id + + def full_seller_onboarding( + self, + seller: Seller, + products: list[Product], + quantities: list[int], + warehouse: Warehouse, + ) -> str: + self.register_seller(seller.get_seller_id()) + + for p in products: + seller.add_product(p) + self.list_product(p.get_product_id()) + + result = self.batch_restock(products, quantities, warehouse) + + return "Onboarded seller " + seller.get_seller_id() + ". " + result + + def get_product_dict(self, products: list[Product]) -> dict[str, int]: + return {p.get_product_id(): p.get_price() for p in products} + + def count_high_rated_products( + self, products: list[Product], min_rating: int + ) -> int: + high_rated = [p for p in products if p.get_average_rating() >= min_rating] + return len(high_rated) + + def get_ret_tuple(self, product: Product) -> tuple[int, int]: + price = product.get_price() + stock = product.get_stock() + return (price, stock) + + def unpack_and_use_tuple(self, product: Product) -> str: + price, stock = self.get_ret_tuple(product) + return "Price: " + str(price) + ", Stock: " + str(stock) + + def nested_comprehension_test( + self, sellers: list[Seller], products: list[Product] + ) -> list[int]: + return [ + sum([p.get_price() for p in products if p.get_seller() == s]) + for s in sellers + ] + + def dispatch_all_pending(self, warehouse: Warehouse, order_ids: list[str]) -> int: + dispatched = 0 + for order_id in order_ids: + warehouse.dispatch_shipment(order_id) + dispatched += 1 + return dispatched diff --git a/obol/examples/original/user_item.py b/obol/examples/original/user_item.py new file mode 100644 index 00000000..d20d8280 --- /dev/null +++ b/obol/examples/original/user_item.py @@ -0,0 +1,315 @@ +from obol.api import entity, send_async, gather +from typing import Optional +import logging +from typing import TypeVar, Type, Callable, Any + + + +class NotEnoughBalance(Exception): + pass + + +class OutOfStock(Exception): + pass + + +@entity +class Coupon: + def __init__(self, code: str, discount: int): + self.code: str = code + self.discount: int = discount + + def __key__(self) -> str: + return self.code + + def get_discount(self) -> int: + return self.discount + + +@entity +class Item: + def __init__(self, item_name: str, price: int): + self.item_name: str = item_name + self.stock: int = 0 + self.price: int = price + + def __key__(self) -> str: + return self.item_name + + def get_price(self) -> int: + return self.price + + def get_stock(self) -> int: + return self.stock + + def update_stock(self, amount: int) -> bool: + if (self.stock + amount) < 0: + raise OutOfStock("Not enough stock to update.") + self.stock += amount + return True + + +@entity +class User: + def __init__(self, username: str): + self.username: str = username + self.balance: int = 0 + self.myitems: list[Item] = [] + + def __key__(self) -> str: + return self.username + + def get_balance(self) -> int: + return self.balance + + def get_items(self) -> list[Item]: + return self.myitems + + def add_balance(self, amount: int) -> bool: + self.balance += amount + return True + + + def simple_loop(self, items: list[Item]) -> int: + total = 0 + for item in items: + total += item.get_price() + + return total + + def buy_item(self, amount: int, item: Item) -> bool: + total_price = amount * item.get_price() + + if self.balance < total_price: + raise NotEnoughBalance("Not enough balance to buy the item.") + + item.update_stock(-amount) + + self.balance -= total_price + self.myitems.append(item) + return True + + def drain_stock(self, item: Item) -> int: + total = 0 + + while 0 < (item.get_stock() - 1): + item.update_stock(-1) + total += 1 + return total + + def discounted_sum(self, items: list[Item], threshold: int) -> int: + if not items: + return 0 + price = items[0].get_price() + rest = self.discounted_sum(items[1:], threshold) + if price > threshold: + return rest + int(price * 0.9) + return rest + price + + + def bulk_purchase_with_tiers(self, cart: list[Item], quantities: list[int]) -> str: + total_cost = 0 + + for index in range(len(cart)): + item = cart[index] + requested_amount = quantities[index] + + if item.get_stock() >= requested_amount: + current_item_cost = 0 + + for unit in range(1, requested_amount + 1): + if unit > 50: + current_item_cost += int(item.get_price() * 0.8) + elif unit > 10: + current_item_cost += int(item.get_price() * 0.9) + else: + current_item_cost += item.get_price() + + if (total_cost + current_item_cost) > self.balance: + raise NotEnoughBalance("Cannot afford the entire cart.") + + item.update_stock(-requested_amount) + total_cost += current_item_cost + + for _ in range(requested_amount): + self.myitems.append(item) + else: + logging.warning(f"Skipping {item} due to low stock.") + + self.balance -= total_cost + return "Bulk purchase complete. Remaining balance: " + str(self.balance) + + def inventory_value(self) -> int: + return sum([item.get_price() for item in self.myitems if item.get_price() > 20]) + + def my_item_prices(self) -> list[int]: + return [item.get_price() for item in self.myitems] + + def ret_tuple(self, item: Item) -> tuple[int, int]: + return (item.get_price(), item.get_stock()) + + def ret_dict(self, item: Item) -> dict[str, int]: + price, stock = self.ret_tuple(item) + return {"price": price, "stock": stock} + + def fire_and_forget(self, item: Item) -> None: + send_async(item.update_stock(1)) + + def demo(self) -> str: + for item in self.myitems: + for _ in range(100): + send_async(self.helper(item)) + return "demo complete" + + def helper(self, item: Item) -> int: + item.update_stock(1) + return 1 + + def demo2(self, item: Optional[Item] = None) -> str: + if item is None: + return "No item provided" + item.update_stock(1) + return "demo complete" + + def recursion_test(self, items: list[Item]) -> int: + if not items: + return 0 + return items[0].get_price() + self.recursion_test(items[1:]) + + def comprehensions(self, items: list[Item]) -> dict[Item, int]: + return {item: item.get_stock() for item in items} + + def type_test(self, hard: list[list[dict[Item, int]]], easy: list[list[Item]]) -> str: + temp = easy[0][0] + temp.get_stock() + + list(hard[0][0].keys())[0].get_stock() + + temp4 = self.myitems[0] + temp4.get_stock() + + stock_val = self.myitems[0].get_stock() + + lst = [self.myitems[0], self.myitems[1]] + stock = lst[0].get_stock() + + return "hello" + + def process_cart_with_limits(self, cart: list[Item], max_spend: int) -> dict: + purchased = {} + total_spent = 0 + + for item in cart: + price = item.get_price() + + if price > self.balance: + continue # can't afford even one, skip + + if total_spent >= max_spend: + break # hit the spending cap, stop processing cart + + units_bought = 0 + while 0 < item.get_stock(): + if total_spent + price > max_spend: + break # inner break: this item would exceed cap + if price > self.balance: + break # inner break: ran out of personal balance mid-item + item.update_stock(-1) + self.balance -= price + total_spent += price + units_bought += 1 + + if units_bought > 0: + purchased[item] = units_bought + + return purchased + + def transfer_balance(self, recipient: 'User', amount: int) -> bool: + if self.balance < amount: + raise NotEnoughBalance("Insufficient balance for transfer.") + self.balance -= amount + recipient.add_balance(amount) + return True + + def multi_restock(self, items: list[Item], amounts: list[int]) -> int: + total_added = 0 + for item, amount in zip(items, amounts): + item.update_stock(amount) + total_added += amount + return total_added + + def most_valuable_item_price(self) -> int: + if not self.myitems: + return 0 + return max([item.get_price() for item in self.myitems]) + + def can_afford_cart(self, items: list[Item]) -> bool: + total = sum([item.get_price() for item in items]) + return self.balance >= total + + def group_items_by_price_bucket(self, items: list[Item]) -> dict: + return { + 'cheap': [item.get_price() for item in items if item.get_price() < 20], + 'mid': [item.get_price() for item in items if 20 <= item.get_price() <= 100], + 'expensive': [item.get_price() for item in items if item.get_price() > 100], + } + + def is_in_stock(self, item: Item) -> bool: + return item is not None and item.get_stock() > 0 + + + + def get_discounted_price(self, item: Item, coupon: Coupon) -> int: + price, discount = gather(item.get_price(), coupon.get_discount()) + + discounted_price = price - discount + return max(discounted_price, 0) + + def buy_with_coupon(self, item: Item, coupon: Optional[Coupon]) -> bool: + if coupon is None: + return self.buy_item(1, item) + + discounted_price = self.get_discounted_price(item, coupon) + + if self.balance < discounted_price: + raise NotEnoughBalance("Not enough balance to buy the item with coupon.") + if not self.is_in_stock(item): + raise OutOfStock("Item is out of stock.") + + item.update_stock(-1) + self.balance -= discounted_price + self.myitems.append(item) + return True + + + def gather_in_loop(self, items: list[Item], coupons: list[Coupon]) -> int: + total = 0 + for item, coupon in zip(items, coupons): + price, discount = gather(item.get_price(), coupon.get_discount()) + discounted_price = price - discount + total += discounted_price + return total + + def inventory_value_gather(self) -> int: + + prices = gather(*[item.get_price() for item in self.myitems]) + return sum(list(prices)) + + def reference_test(self, item: Item) -> list[int]: + + list_1 = [1, 2, 3] + + list_2 = list_1 + + list_2.append(4) + + item.get_price() + + list_2.append(5) + list_1.append(6) + + return list_1 + + def price_check(self, a: Item, b: Item, coupon: Coupon) -> int: + pa, pb, d = gather(a.get_price(), b.get_price(), coupon.get_discount()) + return max(pa + pb - d, 0) diff --git a/obol/pyproject.toml b/obol/pyproject.toml new file mode 100644 index 00000000..3706a2dc --- /dev/null +++ b/obol/pyproject.toml @@ -0,0 +1,139 @@ +[project] +name = "obol" +version = "0.4.0" +authors = [ + { name = "Alexandros Dimakos", email = "S.A.Dimakos@student.tudelft.nl" }, + { name = "Jeff Smits", email = "J.Smits-1@tudelft.nl" }, +] +description = "Compiler for the Styx embedded DSL in Python" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "libcst>=1.8.6", + "libcst-dfa>=0.0.1", + "libcst-mypy>=0.1.0", + "mypy>=1.19.1", + "styx", +] + +[dependency-groups] +dev = [ + "hatch>=1.13.0", + "prek>=0.3.6", + "pytest>=9.0.2", + "pytest-cov>=7.0.0", + "mkdocstrings[python]>=1.0.3", + "mkdocs-material>=9.7.5", + "setuptools-scm>=9.2.2", + "sanic>=25.12.1", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/obol"] + +[tool.hatch.build.targets.sdist] +include = ["src/obol", "README.md", "LICENSE", "pyproject.toml"] + +[tool.uv] +no-binary-package = ["mypy"] + +[tool.uv.sources] +styx = { path = "../styx-package", editable = true } + +[project.urls] +Homepage = "https://github.com/AlexDimakos/styx" + +[project.scripts] +obol = "obol.core:main" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +filterwarnings = [ + "error", +] +log_cli_level = "INFO" +testpaths = [ + "tests", +] + +[tool.coverage] +run.source = ["obol"] +port.exclude_lines = [ + 'pragma: no cover', + '\.\.\.', + 'if typing.TYPE_CHECKING:', +] + +[tool.ruff] +src = ["src"] +exclude = ["examples"] # compiler inputs / generated output: don't lint or format +line-length = 120 # how long you want lines to be + +[tool.ruff.format] +docstring-code-format = true # code snippets in docstrings will be formatted + +[tool.ruff.lint] +# For more strict linting and code simplifications, replace this with +# https://github.com/NLeSC/python-template/blob/main/template/pyproject.toml.jinja#L121-L136 +select = [ + "E", "F", "W", # flake8 + "B", # flake8-bugbear + "I", # isort + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "UP", # pyupgrade + "YTT", # flake8-2020 + "EXE", # flake8-executable + # "D", # pydocstyle, uncomment to have the docstrings linted + # "ANN", # flake8-annotations, uncomment to have type hint annotations linted +] +ignore = [ + "PLR", # Design related pylint codes + "ISC001", # Conflicts with formatter +] +pydocstyle.convention = "google" +exclude = [ + "examples/**.py" +] + +[tool.ruff.lint.per-file-ignores] +# Tests can ignore a few extra rules +"tests/**.py" = [ + "ANN201", # Missing return type annotation for public function + "D103", # Missing docstring + "S101", # Use of assert is detected + "INP001", # Missing __init__.py file + "PT009", # unittest-style assertions are idiomatic in a TestCase + "PLW0603", # module-global Styx client set up in setUpModule + "EM101", # exception message string literals are fine in tests + "EM102", # f-string exception messages are fine in tests +] +# The demo app uses web-framework handler signatures. +"working-example/**.py" = [ + "ARG001", # Sanic passes request/app/loop to handlers even when unused + "INP001", # standalone scripts, not an importable package +] + +[tool.yamlfix] +line_length = 120 +sequence_style = "keep_style" +explicit_start = false +whitelines = 1 +section_whitelines = 1 diff --git a/obol/src/__init__.py b/obol/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/obol/src/obol/__init__.py b/obol/src/obol/__init__.py new file mode 100644 index 00000000..bbd3d8d1 --- /dev/null +++ b/obol/src/obol/__init__.py @@ -0,0 +1,47 @@ +""" +Styx Transpiler Package + +A transpiler for converting synchronous code with @entity decorators +into asynchronous stateful functions. +""" + +from __future__ import annotations + +from importlib.metadata import Distribution, version + + +def _is_editable() -> bool: + dist = Distribution.from_name("obol") + + try: + editable = dist.origin.dir_info.editable + except AttributeError: + editable = False + + if not isinstance(editable, bool): + return False + + return editable + + +if _is_editable(): + # Development way of getting the version, it is dynamically updated on each call + try: + # setuptools_scm is not included in the runtime virtual environment + # importing it raises an ImportError, + # it also fails in the gitlab CI with a UserWarning error + from setuptools_scm import get_version + + # This will fail with LookupError if Git is not installed + __version__ = get_version(root="../..", relative_to=__file__) + except ( + ImportError, + LookupError, + UserWarning, + ): + __version__ = version(__name__) +else: + # Get the version as specified by the wheel + __version__ = version(__name__) + +__all__ = ("__version__",) diff --git a/obol/src/obol/api.py b/obol/src/obol/api.py new file mode 100644 index 00000000..480dfa15 --- /dev/null +++ b/obol/src/obol/api.py @@ -0,0 +1,81 @@ +from typing import Any, overload + + +def entity[T](cls: type[T]) -> type[T]: + """Decorator to mark a class as a Styx stateful entity.""" + return cls + + +def send_async(remote_call: Any) -> None: + """ + Wrap a remote call to execute asynchronously (fire-and-forget). + Example: send_async(user.add_money(50)) + """ + + +def get_entity_by_key[T](entity_class: type[T], key: Any) -> T: + """ + Statically resolve an entity reference by its composite or singular key. + This call is expanded by the compiler and cannot be executed locally. + """ + msg = ( + f"Cannot instantiate local reference to remote entity '{entity_class.__name__}'. " + "Did you forget to run the Styx transpiler?" + ) + raise NotImplementedError(msg) + + +def exists(entity: Any) -> bool: + """ + Check whether the entity at this key has been inserted (its state is non-empty). + + Only meaningful as `exists(self)` inside an @entity method — at runtime the + Styx framework dispatches to a fresh empty state when a method is called on + an uninitialized key, so this is the supported way to detect that case. + + Compile-time intrinsic: rewritten to `bool(__state__)`. + """ + msg = "exists() is a compiler intrinsic and cannot be executed locally. Did you forget to run the obol compiler?" + raise NotImplementedError(msg) + + +# 1. Homogeneous / Dynamic Overload +# Matches: gather(*[item.get_price() for item in cart]) +@overload +def gather[T](*args: T) -> tuple[T, ...]: ... + + +# 2. Heterogeneous / Static Overloads +# Matches: gather(item.get_price()) +@overload +def gather[T1](call1: T1) -> tuple[T1]: ... + + +# Matches: gather(item.get_price(), user.get_profile()) +@overload +def gather[T1, T2](call1: T1, call2: T2) -> tuple[T1, T2]: ... + + +# Matches: gather(call1, call2, call3) +@overload +def gather[T1, T2, T3](call1: T1, call2: T2, call3: T3) -> tuple[T1, T2, T3]: ... + + +# Matches: gather(call1, call2, call3, call4) +@overload +def gather[T1, T2, T3, T4](call1: T1, call2: T2, call3: T3, call4: T4) -> tuple[T1, T2, T3, T4]: ... + + +# Matches: gather(call1, call2, call3, call4, call5) +@overload +def gather[T1, T2, T3, T4, T5](call1: T1, call2: T2, call3: T3, call4: T4, call5: T5) -> tuple[T1, T2, T3, T4, T5]: ... + + +def gather(*args: Any) -> Any: + """ + Parallelize multiple remote entity calls (Fan-Out/Fan-In). + This is a obol compiler intrinsic and will be expanded into a + distributed barrier step at compile time. + """ + msg = "gather() is a compiler intrinsic and cannot be executed locally. Did you forget to run the obol compiler?" + raise NotImplementedError(msg) diff --git a/obol/src/obol/comprehension_expander.py b/obol/src/obol/comprehension_expander.py new file mode 100644 index 00000000..49106c9e --- /dev/null +++ b/obol/src/obol/comprehension_expander.py @@ -0,0 +1,283 @@ +""" +Comprehension-to-loop expander. +""" + +import libcst as cst +import libcst.matchers as m + + +def find_gather_spread_comps(node: cst.CSTNode) -> set[int]: + """Return ids of comprehensions used as the spread arg of `gather(*)`. + + Those must stay intact so the processor can fan them out in parallel rather + than serializing them into an accumulator + for-loop. + """ + skip: set[int] = set() + for call in m.findall(node, m.Call(func=m.Name("gather"))): + for arg in call.args: + if arg.star == "*" and isinstance(arg.value, (cst.ListComp, cst.SetComp, cst.GeneratorExp)): + skip.add(id(arg.value)) + return skip + + +def _genexp_contains_remote_call(genexp: cst.GeneratorExp, entities: dict[str, str]) -> bool: + for call in m.findall(genexp, m.Call()): + if isinstance(call.func, cst.Name) and call.func.value in entities: + return True + if isinstance(call.func, cst.Attribute): + return True + return False + + +def _check_unsupported_genexps(stmt: cst.CSTNode, entities: dict[str, str], skip_ids: set[int]) -> None: + """Raise if `stmt` contains a generator expression with a remote call. + + Generator expressions with remote calls would need to be sliced into + continuation steps while preserving lazy evaluation — not yet supported. + + TODO: add support for generator expressions. The lazy semantics mean we + cannot just expand them into an accumulator + for-loop the way we do for + list/set/dict comprehensions, we need a step-by-step evaluation model that + suspends at each yield across remote-call boundaries. + """ + for genexp in m.findall(stmt, m.GeneratorExp()): + if id(genexp) in skip_ids: + continue + if _genexp_contains_remote_call(genexp, entities): + msg = ( + "Generator expressions with remote calls are not currently supported. " + "Rewrite as a list/set/dict comprehension or an explicit for-loop, " + "or use `gather(*)` for parallel fan-out." + ) + raise NotImplementedError(msg) + + +class OutermostCompFinder(cst.CSTVisitor): + """Finds the first (outermost) comprehension in a node to preserve nested scoping.""" + + def __init__(self, skip_ids: set[int] | None = None) -> None: + self.target: cst.CSTNode | None = None + self._skip_ids = skip_ids or set() + + def _check_and_stop(self, node: cst.CSTNode) -> bool: + # Skipped comps (e.g. `gather(*)`) keep descending so inner comps still get found. + if id(node) in self._skip_ids: + return True + if self.target is None: + self.target = node + return False # Always stop traversing children so we only get the outermost + + def visit_ListComp(self, node: cst.ListComp) -> bool: + return self._check_and_stop(node) + + def visit_SetComp(self, node: cst.SetComp) -> bool: + return self._check_and_stop(node) + + def visit_DictComp(self, node: cst.DictComp) -> bool: + return self._check_and_stop(node) + + # GeneratorExp is not currently supported due to its lazy evaluation. + + def visit_FunctionDef(self, _node: cst.FunctionDef) -> bool: + return False + + def visit_ClassDef(self, _node: cst.ClassDef) -> bool: + return False + + +class TargetedReplacer(cst.CSTTransformer): + """Replaces only the specifically targeted comprehension node and builds its loop.""" + + def __init__(self, target: cst.CSTNode, var_name: str) -> None: + super().__init__() + self.target = target + self.var_name = var_name + self.hoisted: list[cst.BaseStatement] = [] + + # Skip children of the target so inner comprehensions stay intact for the next pass + def visit_ListComp(self, node: cst.ListComp) -> bool: + return node is not self.target + + def visit_SetComp(self, node: cst.SetComp) -> bool: + return node is not self.target + + def visit_DictComp(self, node: cst.DictComp) -> bool: + return node is not self.target + + def visit_FunctionDef(self, _node: cst.FunctionDef) -> bool: + return False + + def visit_ClassDef(self, _node: cst.ClassDef) -> bool: + return False + + def leave_ListComp(self, original_node: cst.ListComp, updated_node: cst.ListComp) -> cst.BaseExpression: + if original_node is self.target: + self.hoisted.extend(_build_list_comp_loop(self.var_name, original_node)) + return cst.Name(self.var_name) + return updated_node + + def leave_SetComp(self, original_node: cst.SetComp, updated_node: cst.SetComp) -> cst.BaseExpression: + if original_node is self.target: + self.hoisted.extend(_build_set_comp_loop(self.var_name, original_node)) + return cst.Name(self.var_name) + return updated_node + + def leave_DictComp(self, original_node: cst.DictComp, updated_node: cst.DictComp) -> cst.BaseExpression: + if original_node is self.target: + self.hoisted.extend(_build_dict_comp_loop(self.var_name, original_node)) + return cst.Name(self.var_name) + return updated_node + + +class ComprehensionExpander(cst.CSTTransformer): + def __init__(self, entities: dict[str, str] | None = None) -> None: + super().__init__() + self.entities = entities or {} + self.current_class: str | None = None + self._counter = 0 + + # Stack to handle nested functions (visit happens twice before leave) + self._counter_stack: list[int] = [] + + def visit_ClassDef(self, node: cst.ClassDef) -> bool: + self.current_class = node.name.value + return True + + def leave_ClassDef(self, _original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.ClassDef: + self.current_class = None + return updated_node + + def visit_FunctionDef(self, _node: cst.FunctionDef) -> bool: + self._counter_stack.append(self._counter) + self._counter = 0 + return True + + def leave_FunctionDef(self, _original_node, updated_node): + self._counter = self._counter_stack.pop() + return updated_node + + def _next_var(self) -> str: + self._counter += 1 + return f"_comp_result_{self._counter}" + + def leave_IndentedBlock( + self, _original_node: cst.IndentedBlock, updated_node: cst.IndentedBlock + ) -> cst.IndentedBlock: + return self._process_block(updated_node) + + def leave_Module(self, _original_node: cst.Module, updated_node: cst.Module) -> cst.Module: + return self._process_block(updated_node) + + def _process_block(self, node: cst.IndentedBlock | cst.Module) -> cst.IndentedBlock | cst.Module: + # Only expand if we are NOT in a class OR if we are in an entity class + if self.current_class is not None and self.current_class not in self.entities: + return node + + modified = True + current_body = list(node.body) + + # Keep processing the block until all comprehensions are flattened out. + while modified: + modified = False + new_body = [] + + for stmt in current_body: + # `gather(*)` keeps its comp intact for runtime fan-out. + skip_ids = find_gather_spread_comps(stmt) + _check_unsupported_genexps(stmt, self.entities, skip_ids) + finder = OutermostCompFinder(skip_ids=skip_ids) + stmt.visit(finder) + + if finder.target: + var = self._next_var() + replacer = TargetedReplacer(finder.target, var) + + # 1. Replace the target in the current statement + new_stmt = stmt.visit(replacer) + + # 2. Recursively visit the newly generated loop statements + # otherwise [[item.get_stock() for item in items] for _ in range(3)] wouldn't work + processed_hoisted = [h.visit(self) for h in replacer.hoisted] + + # 3. Append the properly scoped statements + new_body.extend(processed_hoisted) + new_body.append(new_stmt) + + modified = True + else: + new_body.append(stmt) + + current_body = new_body + + return node.with_changes(body=current_body) + + +def _wrap_in_ifs(body: list[cst.BaseStatement], ifs: tuple[cst.CompIf, ...]) -> list[cst.BaseStatement]: + result = body + for comp_if in reversed(ifs): + result = [cst.If(test=comp_if.test, body=cst.IndentedBlock(body=result), leading_lines=[])] + return result + + +def _build_for_chain(for_in: cst.CompFor, innermost_body: list[cst.BaseStatement]) -> cst.For: + if for_in.inner_for_in is not None: + inner_loop = _build_for_chain(for_in.inner_for_in, innermost_body) + body_with_ifs = _wrap_in_ifs([inner_loop], for_in.ifs) + else: + body_with_ifs = _wrap_in_ifs(innermost_body, for_in.ifs) + + is_async = for_in.asynchronous + + return cst.For( + target=for_in.target, iter=for_in.iter, body=cst.IndentedBlock(body=body_with_ifs), asynchronous=is_async + ) + + +def _build_list_comp_loop(var: str, node: cst.ListComp) -> list[cst.BaseStatement]: + init = cst.parse_statement(f"{var} = []") + append = cst.SimpleStatementLine( + body=[ + cst.Expr( + value=cst.Call( + func=cst.Attribute(value=cst.Name(var), attr=cst.Name("append")), + args=[cst.Arg(value=node.elt)], + ) + ) + ] + ) + return [init, _build_for_chain(node.for_in, [append])] + + +def _build_set_comp_loop(var: str, node: cst.SetComp) -> list[cst.BaseStatement]: + init = cst.parse_statement(f"{var} = set()") + add = cst.SimpleStatementLine( + body=[ + cst.Expr( + value=cst.Call( + func=cst.Attribute(value=cst.Name(var), attr=cst.Name("add")), + args=[cst.Arg(value=node.elt)], + ) + ) + ] + ) + return [init, _build_for_chain(node.for_in, [add])] + + +def _build_dict_comp_loop(var: str, node: cst.DictComp) -> list[cst.BaseStatement]: + init = cst.parse_statement(f"{var} = {{}}") + assign = cst.SimpleStatementLine( + body=[ + cst.Assign( + targets=[ + cst.AssignTarget( + target=cst.Subscript( + value=cst.Name(var), + slice=[cst.SubscriptElement(slice=cst.Index(value=node.key))], + ) + ) + ], + value=node.value, + ) + ] + ) + return [init, _build_for_chain(node.for_in, [assign])] diff --git a/obol/src/obol/config.py b/obol/src/obol/config.py new file mode 100644 index 00000000..fce3d397 --- /dev/null +++ b/obol/src/obol/config.py @@ -0,0 +1 @@ +N_PARTITIONS = 4 diff --git a/obol/src/obol/core.py b/obol/src/obol/core.py new file mode 100644 index 00000000..1d62e0a5 --- /dev/null +++ b/obol/src/obol/core.py @@ -0,0 +1,542 @@ +""" +Main Styx transpiler implementation. +""" + +import argparse +import os +import sys +import tempfile +from collections.abc import Mapping +from pathlib import Path + +import libcst as cst +import libcst.matchers as m +import mypy.api +from libcst import CSTNode, FlattenSentinel, FunctionDef, Module, RemovalSentinel +from libcst_dfa.live_variables import LiveVariablesProvider +from libcst_mypy import MypyTypeInferenceProvider +from libcst_mypy.utils import MypyType + +from obol.comprehension_expander import ComprehensionExpander +from obol.config import N_PARTITIONS +from obol.processor import FunctionProcessor +from obol.transformers import ( + EntityTypeReplacer, + RemoteCallLinearizer, + ReturnHandlerTransformer, + ShortCircuitRewriter, + StateAccessTransformer, + normalize_function_body, +) +from obol.visitor import EntityDiscoveryVisitor + + +def _uses_state(node: cst.CSTNode) -> bool: + """Recursively checks whether any Name('__state__') appears in the CST subtree.""" + if isinstance(node, cst.Name) and node.value == "__state__": + return True + return any(_uses_state(child) for child in node.children) + + +def _is_gather_join(func: cst.FunctionDef) -> bool: + """Gather-join continuations are emitted with a `_gather_partial` parameter.""" + return any(p.name.value == "_gather_partial" for p in func.params.params) + + +def _gather_state_load_index(func: cst.FunctionDef, body_list: list) -> int: + """Where to insert `__state__ = ctx.get() or {}` in a step body so that we don't + create many deep copies of the state before all results are collected.""" + + if not _is_gather_join(func): + return 0 + for idx, stmt in enumerate(body_list): + if isinstance(stmt, cst.If): + return idx + 1 + return 0 + + +class StyxTransformer(cst.CSTTransformer): + """ + Main transformer that processes entity classes and converts them to Styx operators. + """ + + def __init__( + self, + entities: dict[str, str], + metadata: Mapping, + entity_keys: dict[str, list[str]] | None = None, + entity_init_params: dict[str, list[str]] | None = None, + live_vars: Mapping | None = None, + ): + super().__init__() + self.entities = entities + self.metadata = metadata + self.entity_keys = entity_keys or {} + self.entity_init_params = entity_init_params or {} + self.live_vars = live_vars or {} + self.current_operator = None + + def visit_ClassDef(self, node: cst.ClassDef) -> bool: + if node.name.value in self.entities: + self.current_operator = node.name.value + return True + return False + + def leave_Module(self, _original_node: cst.Module, updated_node: cst.Module) -> cst.Module: + imports = [ + cst.SimpleStatementLine(body=[cst.parse_statement("from styx.common.operator import Operator").body[0]]), + cst.SimpleStatementLine( + body=[cst.parse_statement("from styx.common.stateful_function import StatefulFunction").body[0]] + ), + cst.SimpleStatementLine(body=[cst.parse_statement("from styx.common.logging import logging").body[0]]), + cst.EmptyLine(), + ] + + helpers_code = """ +def send_reply(ctx: StatefulFunction, reply_to: list, result): + if reply_to: + reply_info = reply_to[-1] + if isinstance(reply_info, dict) and reply_info.get("sink"): + return + ctx.call_remote_async( + operator_name=reply_info["op_name"], + function_name=reply_info["fun"], + key=reply_info["id"], + params=(reply_info["context"], result, reply_to[:-1]), + ) + else: + return result + + +def push_continuation( + ctx: StatefulFunction, reply_to: list, op_name: str, fun: str, step_id: str, context: dict +) -> list: + context_dict = ctx.get_func_context() or {} + next_id = context_dict.get("next_id", 0) + context_dict["next_id"] = next_id + 1 + + context_dict[next_id] = context + ctx.put_func_context(context_dict) + if reply_to is None: + reply_to = [] + reply_to.append( + { + "op_name": op_name, + "fun": fun, + "id": step_id, + "context": next_id, + } + ) + return reply_to + + +def resolve_context(ctx: StatefulFunction, context_data) -> dict: + if isinstance(context_data, dict): + return context_data + + ctx_dict = ctx.get_func_context() or {} + params = ctx_dict.pop(context_data) + ctx.put_func_context(ctx_dict) + return params + + +def init_gather_barrier(ctx: StatefulFunction, total: int, saved: dict, parent_reply_to) -> str: + ctx_dict = ctx.get_func_context() or {} + counter = ctx_dict.get("_gather_counter", 0) + barrier_id = "_gather_" + str(counter) + ctx_dict["_gather_counter"] = counter + 1 + ctx_dict[barrier_id] = { + "total": total, + "pending": {}, + "saved": saved, + "parent_reply_to": parent_reply_to, + } + ctx.put_func_context(ctx_dict) + return barrier_id + + +def update_gather_barrier(ctx: StatefulFunction, barrier_id: str, tag, result): + ctx_dict = ctx.get_func_context() or {} + barrier = ctx_dict[barrier_id] + if barrier["total"] == 0: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + return True, (), barrier["saved"], barrier["parent_reply_to"] + barrier["pending"][tag] = result + if len(barrier["pending"]) == barrier["total"]: + ctx_dict.pop(barrier_id) + ctx.put_func_context(ctx_dict) + results = tuple(barrier["pending"][i] for i in range(barrier["total"])) + return True, results, barrier["saved"], barrier["parent_reply_to"] + ctx.put_func_context(ctx_dict) + return False, None, None, None +""" + helpers_module = cst.parse_module(helpers_code) + helpers = [*list(helpers_module.body), cst.EmptyLine()] + + # Filter out stuff for mytype (entity function, logging class) from body + stub_names = {"entity", "logging", "send_async"} + + # Matches `from obol.api import …` and `from obol import api[, …]`. + styx_api_dotted = m.SimpleStatementLine( + body=[ + m.ZeroOrMore(), + m.ImportFrom(module=m.Attribute(value=m.Name("obol"), attr=m.Name("api"))), + m.ZeroOrMore(), + ] + ) + styx_api_bare = m.SimpleStatementLine( + body=[ + m.ZeroOrMore(), + m.ImportFrom( + module=m.Name("obol"), + names=[m.ZeroOrMore(), m.ImportAlias(name=m.Name("api")), m.ZeroOrMore()], + ), + m.ZeroOrMore(), + ] + ) + + def is_styx_api_import(node: cst.CSTNode) -> bool: + return m.matches(node, styx_api_dotted) or m.matches(node, styx_api_bare) + + filtered_body = [ + stmt + for stmt in updated_node.body + if not ( + (isinstance(stmt, cst.FunctionDef) and stmt.name.value in stub_names) + or (isinstance(stmt, cst.ClassDef) and stmt.name.value in stub_names) + or is_styx_api_import(stmt) + ) + ] + + new_body = list(imports) + helpers + filtered_body + return updated_node.with_changes(body=new_body) + + def leave_ClassDef( + self, original_node: cst.ClassDef, updated_node: cst.ClassDef + ) -> cst.ClassDef | cst.FlattenSentinel: + if original_node.name.value not in self.entities: + return updated_node + + op_name = self.entities[original_node.name.value] + + entity_name = original_node.name.value + keys = self.entity_keys.get(entity_name, []) + if len(keys) > 1: + op_def_code = ( + f"{op_name}_operator = Operator(" + f"'{op_name}', n_partitions={N_PARTITIONS}, composite_key_hash_params=(0, ':'))" + ) + else: + op_def_code = f"{op_name}_operator = Operator('{op_name}', n_partitions={N_PARTITIONS})" + op_def_node = cst.parse_statement(op_def_code) + + new_nodes = [op_def_node, cst.EmptyLine()] + + for statement in updated_node.body.body: + if isinstance(statement, (cst.FunctionDef, cst.ClassDef)): + new_nodes.append(statement) + new_nodes.append(cst.EmptyLine()) + + return cst.FlattenSentinel(new_nodes) + + def leave_FunctionDef( + self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef + ) -> FunctionDef | RemovalSentinel | FlattenSentinel: + if self.current_operator is None: + return updated_node + + # Normalize inline function bodies (SimpleStatementSuite -> IndentedBlock) + original_node = normalize_function_body(original_node) + updated_node = normalize_function_body(updated_node) + + func_name = original_node.name.value + + if func_name == "__key__": + return cst.RemoveFromParent() + if func_name == "__init__": + prepared = self._prepare_init(updated_node) + return self._process_and_finalize(prepared, prepared, is_init=True) + return self._process_and_finalize(original_node, updated_node, is_init=False) + + def _operator_decorator(self) -> cst.Decorator: + op_name = self.entities[self.current_operator] + "_operator" + return cst.Decorator(decorator=cst.Attribute(value=cst.Name(op_name), attr=cst.Name("register"))) + + def _prepare_init(self, node: cst.FunctionDef) -> cst.FunctionDef: + """Convert __init__ into the entry-point `insert` step before splitting: + rename, drop self, add ctx + reply_to, wrap body with state init and + `return ctx.key`, become async, attach operator decorator. Any remote + calls inside __init__ get sliced normally by FunctionProcessor afterwards. + """ + ctx_param = cst.Param(name=cst.Name("ctx"), annotation=cst.Annotation(annotation=cst.Name("StatefulFunction"))) + reply_to_param = cst.Param( + name=cst.Name("reply_to"), + annotation=cst.Annotation(annotation=cst.Name("list")), + default=cst.Name("None"), + ) + new_params = [ctx_param] + [p for p in node.params.params if p.name.value != "self"] + [reply_to_param] + + init_state = cst.parse_statement("__state__ = {}") + put_func_state = cst.parse_statement("ctx.put_func_context({})") + return_stmt = cst.parse_statement("return ctx.key") + new_block = node.body.with_changes(body=[init_state, *list(node.body.body), put_func_state, return_stmt]) + + return node.with_changes( + name=cst.Name("insert"), + params=node.params.with_changes(params=new_params), + body=new_block, + asynchronous=cst.Asynchronous(), + decorators=[self._operator_decorator()], + ) + + def _process_and_finalize( + self, + original_node: cst.FunctionDef, + updated_node: cst.FunctionDef, + is_init: bool, + ) -> cst.FlattenSentinel: + """Split the function into steps and post-process each one uniformly.""" + processor = FunctionProcessor( + original_node, + self.current_operator, + self.entities, + self.metadata, + self.entity_keys, + self.entity_init_params, + self.live_vars, + ) + new_functions = processor.process() + + final_nodes: list[cst.CSTNode] = [] + for func in new_functions: + is_root = func.name.value == original_node.name.value + + transformed_func = func.visit( + StateAccessTransformer(self.metadata, self.entity_keys, self.entity_init_params) + ) + + # Continuations (and non-init roots) that touch `__state__` must load it + # from ctx. Skip for the init root — its body starts with `__state__ = {}`. + # `or {}` covers the fresh-key case (e.g. method called before any insert). + if _uses_state(transformed_func) and not (is_init and is_root): + get_state = cst.parse_statement("__state__ = ctx.get() or {}") + body_list = list(transformed_func.body.body) + insert_at = _gather_state_load_index(transformed_func, body_list) + body_list.insert(insert_at, get_state) + transformed_func = transformed_func.with_changes(body=cst.IndentedBlock(body=body_list)) + + transformed_func = transformed_func.visit( + ReturnHandlerTransformer(uses_state=_uses_state(transformed_func)) + ) + + # Method root: its signature still has `self` and no decorator. Init root + # already had params/decorator/async set in _prepare_init. + if is_root and not is_init: + transformed_func = self._finalize_original_signature(transformed_func, updated_node) + + final_nodes.append(transformed_func) + final_nodes.append(cst.EmptyLine()) + + return cst.FlattenSentinel(final_nodes) + + def _finalize_original_signature(self, node: cst.FunctionDef, reference_node: cst.FunctionDef): + ctx_param = cst.Param(name=cst.Name("ctx"), annotation=cst.Annotation(cst.Name("StatefulFunction"))) + reply_to_param = cst.Param( + name=cst.Name("reply_to"), annotation=cst.Annotation(cst.Name("list")), default=cst.Name("None") + ) + + new_params = ( + [ctx_param] + [p for p in reference_node.params.params if p.name.value != "self"] + [reply_to_param] + ) + + return node.with_changes( + params=node.params.with_changes(params=new_params), + decorators=[self._operator_decorator()], + asynchronous=cst.Asynchronous(), + ) + + +class StyxTranspiler: + """ + Main transpiler class that orchestrates the transformation process. + """ + + def __init__(self, source_code: str): + self.source_code = source_code + self.cst_tree = cst.parse_module(source_code) + self.entities: dict[str, str] = {} + self.entity_keys = None + self.entity_init_params = None + + def run(self) -> str: + """ + Run the transpilation process. + + Returns: + str: The transpiled code + """ + print("--- Starting Transpilation ---") + + # 1. Discover entities + visitor = EntityDiscoveryVisitor() + self.cst_tree.visit(visitor) + self.entities = visitor.entities + self.entity_keys = visitor.entity_keys + self.entity_key_types = visitor.entity_key_types + self.entity_init_params = visitor.entity_init_params + print(f"Identified {len(self.entities)} stateful entities:", list(self.entities.keys())) + + # 1.5. Expand comprehensions into for loops and rewrite short-circuiting BoolOps with remote calls into + # explicit ifs, to maintain short circuit semantics + expander = ComprehensionExpander(self.entities) + self.cst_tree = self.cst_tree.visit(expander) + + sc_rewriter = ShortCircuitRewriter(self.entities) + self.cst_tree = self.cst_tree.visit(sc_rewriter) + + # 2. Linearize + linearizer = RemoteCallLinearizer(self.entities) + linearized_tree = self.cst_tree.visit(linearizer) + linearized_code = linearized_tree.code + + # 3. Run mypy + live variable analysis on the linearized code to get type and live variable metadata + module, metadata, live_vars = StyxTranspiler._resolve_types(linearized_code) + + # 4. Transform using the same node tree (metadata lookups match) + transformer = StyxTransformer(self.entities, metadata, self.entity_keys, self.entity_init_params, live_vars) + modified_tree = module.visit(transformer) + + # 5. Replace entity type annotations with key types + type_replacer = EntityTypeReplacer(self.entity_keys, self.entity_init_params, self.entity_key_types) + modified_tree = modified_tree.visit(type_replacer) + + return modified_tree.code + + # TODO: This should probably be fixed at some point + # Minimal stubs written alongside the source so mypy resolves + # `from obol.api import …` locally instead of following the + # import into the full installed package. + _API_STUBS = ( + "from typing import TypeVar, Type, Callable, Any, Tuple\n" + "T = TypeVar('T')\n" + "def entity(cls: Type[T]) -> Type[T]: return cls\n" + "def user_operator(func: Callable) -> Callable: return func\n" + "def send_async(remote_call: Any) -> None: ...\n" + "def get_entity_by_key(entity_class: Type[T], key: Any) -> T: ...\n" + "def gather(*args: Any) -> Tuple[Any, ...]: ...\n" + "def exists(entity: Any) -> bool: ...\n" + ) + + @staticmethod + def _resolve_types(source_code: str) -> tuple[Module, Mapping[CSTNode, MypyType], Mapping]: + """ + Run mypy on the source code and return (parsed_module, type_metadata, live_var_metadata). + The type_metadata maps cst.CSTNode -> MypyType. + The live_var_metadata maps cst.CSTNode -> (live_in, live_out). + """ + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) / "source.py" + tmp_path.write_text(source_code, encoding="utf-8") + + # Write a minimal local obol stub package so mypy only + # type-checks our tiny API stubs, not the entire installed package. + stub_pkg = Path(tmp_dir) / "obol" + stub_pkg.mkdir() + (stub_pkg / "__init__.py").write_text("", encoding="utf-8") + (stub_pkg / "api.py").write_text(StyxTranspiler._API_STUBS, encoding="utf-8") + + # Point MYPYPATH at tmp_dir so mypy finds our local stub package + # first and never traverses the full installed obol package. + old_mypypath = os.environ.get("MYPYPATH") + os.environ["MYPYPATH"] = str(tmp_dir) + try: + # 1. First run: type-check and surface user errors + stdout, _stderr, exit_code = mypy.api.run( + [ + "--disable-error-code=func-returns-value", + "--follow-imports=silent", + "--ignore-missing-imports", + "--no-error-summary", + str(tmp_path), + ] + ) + if exit_code != 0: + clean_errs = stdout.replace(str(tmp_path), "source") + msg = f"Mypy Type Check Failed:\n{clean_errs}" + raise RuntimeError(msg) + + # 2. Second run: generate metadata cache + cache = MypyTypeInferenceProvider.gen_cache( + root_path=tmp_path.parent, + paths=[str(tmp_path)], + ) + + module = cst.parse_module(source_code) + file_cache = cache.get(str(tmp_path)) + if not file_cache: + msg = "Mypy failed to generate type metadata. Ensure the code is type-safe." + raise RuntimeError(msg) + + wrapper = cst.metadata.MetadataWrapper( + module, + unsafe_skip_copy=True, + cache={MypyTypeInferenceProvider: file_cache}, + ) + metadata = wrapper.resolve(MypyTypeInferenceProvider) + live_vars = wrapper.resolve(LiveVariablesProvider) + + return wrapper.module, metadata, live_vars + except Exception as e: + if isinstance(e, RuntimeError): + raise + msg = f"Type resolution failed: {e}" + raise RuntimeError(msg) from e + finally: + if old_mypypath is None: + os.environ.pop("MYPYPATH", None) + else: + os.environ["MYPYPATH"] = old_mypypath + + +# Main execution +def main(): + parser = argparse.ArgumentParser( + prog="obol", + description="Compile a sequential Obol entity program into Styx operator functions.", + ) + parser.add_argument( + "input", + type=Path, + nargs="?", + default=Path("examples/original/user_item.py"), + help="path to the input .py file to compile (default: examples/original/user_item.py)", + ) + parser.add_argument( + "output", + type=Path, + nargs="?", + default=None, + help="path to write the compiled output (default: examples/compiled/)", + ) + args = parser.parse_args() + + output_file = args.output if args.output is not None else Path("examples/compiled") / args.input.name + + try: + code = args.input.read_text(encoding="utf-8") + except FileNotFoundError: + print(f"Error: input file '{args.input}' was not found.", file=sys.stderr) + sys.exit(1) + + transpiler = StyxTranspiler(code) + output_code = transpiler.run() + + output_file.parent.mkdir(parents=True, exist_ok=True) + output_file.write_text(output_code, encoding="utf-8") + + print(f"Successfully transpiled '{args.input}' to '{output_file}'") + + +if __name__ == "__main__": + main() diff --git a/obol/src/obol/cst_helpers.py b/obol/src/obol/cst_helpers.py new file mode 100644 index 00000000..93359b0c --- /dev/null +++ b/obol/src/obol/cst_helpers.py @@ -0,0 +1,221 @@ +"""CST construction helpers for reocurring patterns in the compiler""" + +from collections.abc import Iterable + +import libcst as cst + +CTX_KEY = cst.Attribute(value=cst.Name("ctx"), attr=cst.Name("key")) + + +def quoted_str(s: str) -> cst.SimpleString: + return cst.SimpleString(f"'{s}'") + + +def name_or_none(s: str | None) -> cst.BaseExpression: + return cst.Name(s) if s else cst.Name("None") + + +# ── statement primitives ─────────────────────────────────────────────── + + +def assign_stmt(target: cst.BaseExpression, value: cst.BaseExpression) -> cst.SimpleStatementLine: + return cst.SimpleStatementLine(body=[cst.Assign(targets=[cst.AssignTarget(target=target)], value=value)]) + + +def expr_stmt(value: cst.BaseExpression) -> cst.SimpleStatementLine: + return cst.SimpleStatementLine(body=[cst.Expr(value=value)]) + + +# ── containers ───────────────────────────────────────────────────────── + + +def dict_from_pairs(pairs: Iterable[tuple[str, cst.BaseExpression]]) -> cst.Dict: + """Build a dict literal with single-quoted string keys.""" + return cst.Dict(elements=[cst.DictElement(key=quoted_str(k), value=v) for k, v in pairs]) + + +def context_dict(var_names: Iterable[str]) -> cst.Dict: + """{'v1': v1, 'v2': v2, ...} — locals to save across a split.""" + return dict_from_pairs((v, cst.Name(v)) for v in var_names) + + +def tuple_of(elements: Iterable[cst.BaseExpression]) -> cst.Tuple: + return cst.Tuple(elements=[cst.Element(value=e) for e in elements]) + + +# ── call_remote_async ────────────────────────────────────────────────── + + +def call_remote_async( + operator_name: str | cst.BaseExpression, + function_name: str | cst.BaseExpression, + key: cst.BaseExpression, + params: cst.BaseExpression, +) -> cst.Call: + """ctx.call_remote_async(operator_name=..., function_name=..., key=..., params=...). + + String op/fun names are emitted as quoted literals; CST nodes are passed through. + """ + op_val = quoted_str(operator_name) if isinstance(operator_name, str) else operator_name + fun_val = quoted_str(function_name) if isinstance(function_name, str) else function_name + return cst.Call( + func=cst.parse_expression("ctx.call_remote_async"), + args=[ + cst.Arg(keyword=cst.Name("operator_name"), value=op_val), + cst.Arg(keyword=cst.Name("function_name"), value=fun_val), + cst.Arg(keyword=cst.Name("key"), value=key), + cst.Arg(keyword=cst.Name("params"), value=params), + ], + ) + + +def call_remote_async_stmt(operator_name, function_name, key, params) -> cst.SimpleStatementLine: + return expr_stmt(call_remote_async(operator_name, function_name, key, params)) + + +# ── params tuples ────────────────────────────────────────────────────── + + +def params_with_reply_to(args: list[cst.BaseExpression], reply_to: cst.BaseExpression) -> cst.Tuple: + """(arg1, arg2, ..., reply_to) — params= for a remote method call.""" + return tuple_of([*args, reply_to]) + + +def continuation_params( + context: cst.BaseExpression, + result: cst.BaseExpression, + reply_to: cst.BaseExpression, +) -> cst.Tuple: + """(context, result, reply_to) — params= for a direct continuation call.""" + return tuple_of([context, result, reply_to]) + + +# ── reply_to entries ─────────────────────────────────────────────────── + + +def reply_entry( + op_name: str, + fun: str, + key: cst.BaseExpression, + context: cst.BaseExpression, +) -> cst.Dict: + """One reply_to-stack frame: {'op_name': ..., 'fun': ..., 'id': ..., 'context': ...}.""" + return cst.Dict( + elements=[ + cst.DictElement(key=quoted_str("op_name"), value=quoted_str(op_name)), + cst.DictElement(key=quoted_str("fun"), value=quoted_str(fun)), + cst.DictElement(key=quoted_str("id"), value=key), + cst.DictElement(key=quoted_str("context"), value=context), + ] + ) + + +def sink_reply_to() -> cst.List: + """[{'sink': True}] — fire-and-forget marker swallowed by send_reply.""" + return cst.List( + elements=[ + cst.Element(value=cst.Dict(elements=[cst.DictElement(key=quoted_str("sink"), value=cst.Name("True"))])) + ] + ) + + +# ── push_continuation / init_gather_barrier ──────────────────────────── + + +def push_continuation_stmt( + reply_op_name: str, next_func_name: str, context: cst.BaseExpression +) -> cst.SimpleStatementLine: + """reply_to = push_continuation(ctx, reply_to, 'op', 'fun', ctx.key, context).""" + return assign_stmt( + cst.Name("reply_to"), + cst.Call( + func=cst.Name("push_continuation"), + args=[ + cst.Arg(value=cst.Name("ctx")), + cst.Arg(value=cst.Name("reply_to")), + cst.Arg(value=quoted_str(reply_op_name)), + cst.Arg(value=quoted_str(next_func_name)), + cst.Arg(value=CTX_KEY), + cst.Arg(value=context), + ], + ), + ) + + +def init_gather_barrier_stmt(arity: cst.BaseExpression, saved: cst.Dict) -> cst.SimpleStatementLine: + """_gather_id = init_gather_barrier(ctx, arity, saved, reply_to).""" + return assign_stmt( + cst.Name("_gather_id"), + cst.Call( + func=cst.Name("init_gather_barrier"), + args=[ + cst.Arg(value=cst.Name("ctx")), + cst.Arg(value=arity), + cst.Arg(value=saved), + cst.Arg(value=cst.Name("reply_to")), + ], + ), + ) + + +# ── restore block ────────────────────────────────────────────────────── + + +def restore_tuple_from(source: str, var_names: list[str]) -> cst.SimpleStatementLine: + """(v1, v2, ...) = (source.get('v1'), source.get('v2'), ...).""" + targets = cst.Tuple(elements=[cst.Element(cst.Name(v)) for v in var_names]) + values = cst.Tuple( + elements=[ + cst.Element( + cst.Call( + func=cst.Attribute(value=cst.Name(source), attr=cst.Name("get")), + args=[cst.Arg(value=quoted_str(v))], + ) + ) + for v in var_names + ] + ) + return assign_stmt(targets, values) + + +def resolve_context_stmt() -> cst.SimpleStatementLine: + """params = resolve_context(ctx, func_context).""" + return assign_stmt( + cst.Name("params"), + cst.Call( + func=cst.Name("resolve_context"), + args=[cst.Arg(value=cst.Name("ctx")), cst.Arg(value=cst.Name("func_context"))], + ), + ) + + +# ── continuation function def ────────────────────────────────────────── + + +def continuation_func(name: str, body: list, target_var: str, op_name: str) -> cst.FunctionDef: + """Build an `async def (ctx, func_context, =None, reply_to: list = None)` + decorated with operator.register.""" + operator = op_name + "_operator" + deco = cst.Decorator(decorator=cst.parse_expression(f"{operator}.register")) + reply_to_param = cst.Param( + name=cst.Name("reply_to"), + annotation=cst.Annotation(annotation=cst.Name("list")), + default=cst.Name("None"), + ) + return cst.FunctionDef( + name=cst.Name(name), + params=cst.Parameters( + params=[ + cst.Param( + name=cst.Name("ctx"), + annotation=cst.Annotation(cst.Name("StatefulFunction")), + ), + cst.Param(name=cst.Name("func_context")), + cst.Param(name=cst.Name(target_var), default=cst.Name("None")), + reply_to_param, + ] + ), + body=cst.IndentedBlock(body=body), + decorators=[deco], + asynchronous=cst.Asynchronous(), + ) diff --git a/obol/src/obol/entity_resolver.py b/obol/src/obol/entity_resolver.py new file mode 100644 index 00000000..b0e9d11d --- /dev/null +++ b/obol/src/obol/entity_resolver.py @@ -0,0 +1,208 @@ +"""Type and key resolution for remote call detection and routing in the Styx splitter.""" + +from collections.abc import Mapping + +import libcst as cst + + +class EntityResolver: + def __init__( + self, + class_name: str, + original_func: cst.FunctionDef, + entities: dict[str, str], + metadata: Mapping, + entity_keys: dict[str, list[str]] | None = None, + entity_init_params: dict[str, list[str]] | None = None, + ): + self.class_name = class_name + self.original_func = original_func + self.entities = entities + self.metadata = metadata + self.entity_keys = entity_keys or {} + self.entity_init_params = entity_init_params or {} + + # ── entity-type lookup ──────────────────────────────────────────── + + def get_entity_type(self, node: cst.CSTNode) -> str | None: + """Return the entity class name for a node, or None if it's not an entity.""" + mypy_type = self.metadata.get(node) + if mypy_type is not None: + type_name = self._extract_outermost_type_name(mypy_type) + if type_name in self.entities: + return type_name + + if isinstance(node, cst.Call) and isinstance(node.func, cst.Name) and node.func.value in self.entities: + return node.func.value + + # libcst_mypy doesn't attach types to Subscript nodes, so derive the + # element type from the collection's parameterized type + # (e.g. i_ids: list[Item] -> Item). + if isinstance(node, cst.Subscript): + collection_type = self.metadata.get(node.value) + if collection_type is not None: + element_type = self._extract_element_type_name(collection_type) + if element_type in self.entities: + return element_type + + if isinstance(node, cst.Name): + return self._local_entity_binding(node.value) + + return None + + def _local_entity_binding(self, var_name: str) -> str | None: + """Walk this function's body looking for `var_name = ` and return + the entity class name implied by the rhs, or None.""" + for stmt in self.original_func.body.body: + if not isinstance(stmt, cst.SimpleStatementLine): + continue + for el in stmt.body: + target = None + rhs: cst.BaseExpression | None = None + if isinstance(el, cst.Assign) and len(el.targets) == 1: + target = el.targets[0].target + rhs = el.value + elif isinstance(el, cst.AnnAssign): + target = el.target + rhs = el.value + if not (isinstance(target, cst.Name) and target.value == var_name and rhs is not None): + continue + + if isinstance(rhs, cst.Call) and isinstance(rhs.func, cst.Name) and rhs.func.value in self.entities: + return rhs.func.value + + if ( + isinstance(rhs, cst.Call) + and isinstance(rhs.func, cst.Name) + and rhs.func.value == "get_entity_by_key" + and len(rhs.args) >= 1 + and isinstance(rhs.args[0].value, cst.Name) + and rhs.args[0].value.value in self.entities + ): + return rhs.args[0].value.value + return None + + def is_entity_node(self, node: cst.CSTNode) -> bool: + return self.get_entity_type(node) is not None + + @staticmethod + def _extract_outermost_type_name(mypy_type) -> str: + """Outermost class name, ignoring generics and unwrapping `X | None`. + + e.g. 'builtins.list[module.Item]' -> 'list' + 'test_tmp.Item' -> 'Item' + 'test_tmp.Item | None' -> 'Item' + """ + fullname = mypy_type.fullname + # Optional[X] is represented by mypy as 'X | None'. + if " | " in fullname: + parts = [p.strip() for p in fullname.split(" | ") if p.strip() != "None"] + if len(parts) == 1: + fullname = parts[0] + if "[" in fullname: + fullname = fullname.split("[")[0] + return fullname.rsplit(".", 1)[-1] + + @staticmethod + def _extract_element_type_name(mypy_type) -> str | None: + """Element type name from a parameterized list/tuple/dict, or None.""" + + fullname = getattr(mypy_type, "fullname", None) + if not isinstance(fullname, str) or "[" not in fullname or not fullname.endswith("]"): + return None + container = fullname.split("[", 1)[0].rsplit(".", 1)[-1] + inner = fullname[fullname.index("[") + 1 : -1] + + parts = [p.strip() for p in inner.split(",")] + target = parts[-1] if container == "dict" and len(parts) >= 2 else parts[0] + if target in ("...", "Any", ""): + return None + if "[" in target: + target = target.split("[", 1)[0] + return target.rsplit(".", 1)[-1] + + # ── operator + key resolution ───────────────────────────────────── + + def operator_name_for(self, receiver: cst.BaseExpression) -> str: + """Operator routing target for a method/constructor receiver.""" + if isinstance(receiver, cst.Name) and receiver.value in self.entities: + return self.entities[receiver.value] + type_name = self.get_entity_type(receiver) + if type_name: + return self.entities[type_name] + return self.entities.get(self.class_name, "unknown_operator") + + def key_for_call(self, receiver: cst.BaseExpression, call_node: cst.Call, method: str) -> cst.BaseExpression: + """Get the key argument for a remote call. + + For new object instantiations we need to construct it using the constructor arguments and + the def __key__() function which points to which ones they are. + """ + if method == "insert" and isinstance(receiver, cst.Name): + built = self._build_constructor_key(receiver.value, call_node) + if built is not None: + return built + return receiver + + def _build_constructor_key(self, entity_class: str, call_node: cst.Call) -> cst.BaseExpression | None: + key_attrs = self.entity_keys.get(entity_class) + init_params = self.entity_init_params.get(entity_class) + if not (key_attrs and init_params): + return None + + param_names = list(init_params.keys()) + resolved_parts: list[cst.BaseExpression] = [] + for attr in key_attrs: + if attr not in param_names: + continue + idx = param_names.index(attr) + if idx >= len(call_node.args): + continue + arg_val = call_node.args[idx].value + if len(key_attrs) == 1: + return arg_val + resolved_parts.append(cst.Call(func=cst.Name("str"), args=[cst.Arg(value=arg_val)])) + + if len(resolved_parts) <= 1: + return None + + # Composite key: str(a) + ":" + str(b) + ":" + str(c) ... + expr: cst.BaseExpression = resolved_parts[0] + for part in resolved_parts[1:]: + expr = cst.BinaryOperation( + left=cst.BinaryOperation(left=expr, operator=cst.Add(), right=cst.SimpleString('":"')), + operator=cst.Add(), + right=part, + ) + return expr + + # ── remote-call detection ───────────────────────────────────────── + + def is_remote_call(self, stmt: cst.CSTNode) -> bool: + if not isinstance(stmt, cst.SimpleStatementLine) or not stmt.body: + return False + element = stmt.body[0] + if not isinstance(element, (cst.Assign, cst.Expr)): + return False + val = element.value + if not isinstance(val, cst.Call): + return False + + # Ignore self.__key__() we later just replace it with ctx.key + if ( + isinstance(val.func, cst.Attribute) + and isinstance(val.func.value, cst.Name) + and val.func.value.value == "self" + and val.func.attr.value == "__key__" + ): + return False + + # Constructor call: Item(...) + if isinstance(val.func, cst.Name): + return val.func.value in self.entities + + # Method call: item.get_price() + if isinstance(val.func, cst.Attribute): + return self.is_entity_node(val.func.value) + + return False diff --git a/obol/src/obol/liveness.py b/obol/src/obol/liveness.py new file mode 100644 index 00000000..447f9e25 --- /dev/null +++ b/obol/src/obol/liveness.py @@ -0,0 +1,138 @@ +"""Live-variable analysis metadata helpers for the splitter. + +Extracts exact variable names from the metadata and provides utilities for computing +which variables need to be saved at specific split points such as loop entry or exit. +""" + +from collections.abc import Mapping + +import libcst as cst +from libcst import matchers as m +from libcst_dfa.data_flow import ImmutableSet + + +class LivenessHelper: + def __init__(self, live_vars: Mapping | None): + self.live_vars: dict = dict(live_vars) if live_vars is not None else {} + + @staticmethod + def simple_name(v) -> str: + """Convert QualifiedName to the variable name.""" + if hasattr(v, "name"): + # QualifiedName .name may look like 'module.func..var'. + return str(v.name).split(".")[-1] + return str(v).split(".")[-1] if "." in str(v) else str(v) + + def _live_set(self, node: cst.CSTNode | None, kind: str) -> ImmutableSet | None: + if not self.live_vars or node is None: + return None + data = self.live_vars.get(node) + if data is None: + return None + val = data[0] if kind == "in" else data[1] + return val if isinstance(val, ImmutableSet) else None + + def live_out(self, stmt: cst.CSTNode) -> ImmutableSet | None: + """Live-out set for a statement, or None if no liveness data.""" + if isinstance(stmt, cst.SimpleStatementLine) and stmt.body: + element = stmt.body[0] + if isinstance(element, cst.Assign) and element.targets: + live = self._live_set(element.targets[0], "out") + if live is not None: + return live + elif isinstance(element, cst.Expr) and isinstance(element.value, cst.Call): + live = self._live_set(element.value, "out") + if live is not None: + return live + elif isinstance(element, cst.AugAssign): + live = self._live_set(element, "out") + if live is not None: + return live + return self._live_set(stmt, "out") + + def live_in_at_loop(self, loop_stmt: cst.For | cst.While) -> set[str] | None: + """Vars live just before re-entering a loop iteration.""" + if isinstance(loop_stmt, cst.For): + live = self._live_set(loop_stmt, "in") + if live is None: + return None + result = {self.simple_name(qn) for qn in live} + result.update(n.value for n in m.findall(loop_stmt.iter, m.Name())) + return result + + if isinstance(loop_stmt, cst.While): + live = self._live_set(loop_stmt.test, "in") + if live is None: + return None + return {self.simple_name(qn) for qn in live} + return None + + def live_out_at_loop(self, loop_stmt: cst.For | cst.While) -> set[str] | None: + """Vars live just after exiting the loop""" + node = loop_stmt if isinstance(loop_stmt, cst.For) else loop_stmt.test + live = self._live_set(node, "out") + if live is None: + return None + return {self.simple_name(qn) for qn in live} + + def add_synthetic_loop_vars(self, vars_set: set[str], defined_vars: set[str]) -> None: + """Always include synthetic loop index / comp-result vars (generated after the liveness analysis pass).""" + for v in defined_vars: + if v.startswith(("__loop_index_", "_comp_result_")): + vars_set.add(v) + + def vars_to_save_at(self, stmt: cst.CSTNode, defined_vars: set[str]) -> set[str]: + """Variables to save at each split point. + + We use `defined_vars` both as a fallback when liveness data is missing + and to handle cases where scopes differ across conditionals. For example, + a variable may be defined in only one branch of an `if` statement, while + liveness analysis might mark it as live in both branches. Using + `defined_vars` allows us to distinguish between these cases. + """ + live_out = self.live_out(stmt) + if live_out is not None: + live_names = {self.simple_name(qn) for qn in live_out} + result = live_names & defined_vars + self.add_synthetic_loop_vars(result, defined_vars) + return result + return set(defined_vars) + + @staticmethod + def collect_assigned_vars(stmts: list) -> set[str]: + """All variable names assigned anywhere in `stmts`.""" + result: set[str] = set() + for stmt in stmts: + if isinstance(stmt, cst.SimpleStatementLine): + for element in stmt.body: + if isinstance(element, cst.Assign): + for target in element.targets: + if isinstance(target.target, cst.Name) and target.target.value != "__state__": + result.add(target.target.value) + elif isinstance(element, cst.AugAssign): + if isinstance(element.target, cst.Name) and element.target.value != "__state__": + result.add(element.target.value) + elif ( + isinstance(element, cst.AnnAssign) + and isinstance(element.target, cst.Name) + and element.value is not None + and element.target.value != "__state__" + ): + result.add(element.target.value) + elif isinstance(stmt, cst.If): + result.update(LivenessHelper.collect_assigned_vars(list(stmt.body.body))) + if stmt.orelse: + if isinstance(stmt.orelse, cst.Else): + result.update(LivenessHelper.collect_assigned_vars(list(stmt.orelse.body.body))) + elif isinstance(stmt.orelse, cst.If): + result.update(LivenessHelper.collect_assigned_vars([stmt.orelse])) + elif isinstance(stmt, (cst.For, cst.While)): + result.update(LivenessHelper.collect_assigned_vars(list(stmt.body.body))) + if isinstance(stmt, cst.For): + if isinstance(stmt.target, cst.Name): + result.add(stmt.target.value) + elif isinstance(stmt.target, cst.Tuple): + for elem in stmt.target.elements: + if isinstance(elem.value, cst.Name): + result.add(elem.value.value) + return result diff --git a/obol/src/obol/predicates.py b/obol/src/obol/predicates.py new file mode 100644 index 00000000..3433b336 --- /dev/null +++ b/obol/src/obol/predicates.py @@ -0,0 +1,95 @@ +"""Pure structural predicates over libcst nodes used by the splitter.""" + +import libcst as cst + +from obol.entity_resolver import EntityResolver + +# ── statement-shape predicates ──────────────────────────────────────── + + +def is_gather(stmt: cst.CSTNode) -> bool: + """Match ` = gather(...)` or a bare `gather(...)`.""" + if not isinstance(stmt, cst.SimpleStatementLine) or not stmt.body: + return False + element = stmt.body[0] + if not isinstance(element, (cst.Assign, cst.Expr)): + return False + val = element.value + return isinstance(val, cst.Call) and isinstance(val.func, cst.Name) and val.func.value == "gather" + + +def is_continue(stmt: cst.CSTNode) -> bool: + return isinstance(stmt, cst.SimpleStatementLine) and bool(stmt.body) and isinstance(stmt.body[0], cst.Continue) + + +def is_break(stmt: cst.CSTNode) -> bool: + return isinstance(stmt, cst.SimpleStatementLine) and bool(stmt.body) and isinstance(stmt.body[0], cst.Break) + + +def ends_with_raise(body: list) -> bool: + """Last statement is a `raise` — any following code is unreachable.""" + if not body: + return False + last = body[-1] + if isinstance(last, cst.SimpleStatementLine): + return any(isinstance(el, cst.Raise) for el in last.body) + return False + + +def ends_with_terminator(body: list) -> bool: + """Last statement is a `return` or `raise` — any following code is unreachable.""" + if not body: + return False + last = body[-1] + if isinstance(last, cst.SimpleStatementLine): + return any(isinstance(el, (cst.Return, cst.Raise)) for el in last.body) + return False + + +# ── remote-call detection ────────────────────────────────── + + +def any_remote_call(resolver: EntityResolver, stmts: list, loop_context=None) -> bool: + """True if any statement in `stmts` (or any nested if/for/while body) makes + a remote call, gather, orcontinue/break (if inside a loop).""" + for stmt in stmts: + if resolver.is_remote_call(stmt) or is_gather(stmt): + return True + if loop_context and (is_continue(stmt) or is_break(stmt)): + return True + if isinstance(stmt, cst.If): + if any_remote_call(resolver, list(stmt.body.body), loop_context): + return True + if stmt.orelse is not None and ( + ( + isinstance(stmt.orelse, cst.Else) + and any_remote_call(resolver, list(stmt.orelse.body.body), loop_context) + ) + or (isinstance(stmt.orelse, cst.If) and any_remote_call(resolver, [stmt.orelse], loop_context)) + ): + return True + if isinstance(stmt, cst.For) and for_contains_remote_call(resolver, stmt): + return True + if isinstance(stmt, cst.While) and while_contains_remote_call(resolver, stmt): + return True + return False + + +def if_contains_remote_call(resolver: EntityResolver, node: cst.If, loop_context=None) -> bool: + if any_remote_call(resolver, list(node.body.body), loop_context): + return True + if node.orelse is not None: + if isinstance(node.orelse, cst.Else): + if any_remote_call(resolver, list(node.orelse.body.body), loop_context): + return True + elif isinstance(node.orelse, cst.If) and if_contains_remote_call(resolver, node.orelse, loop_context): + return True + return False + + +def for_contains_remote_call(resolver: EntityResolver, node: cst.For) -> bool: + return any_remote_call(resolver, list(node.body.body)) + + +def while_contains_remote_call(resolver: EntityResolver, node: cst.While) -> bool: + return any_remote_call(resolver, list(node.body.body)) diff --git a/obol/src/obol/processor.py b/obol/src/obol/processor.py new file mode 100644 index 00000000..3a7d89c7 --- /dev/null +++ b/obol/src/obol/processor.py @@ -0,0 +1,60 @@ +"""Slices an `@entity` method into chained continuation step functions.""" + +from collections.abc import Mapping + +import libcst as cst + +from obol.entity_resolver import EntityResolver +from obol.liveness import LivenessHelper +from obol.send_async import rewrite_send_async_in_function +from obol.splitting import LoopContext, SplitContext + +__all__ = ["FunctionProcessor", "LoopContext"] + + +class FunctionProcessor: + """Slices a function into asynchronous step functions across remote-call boundaries.""" + + def __init__( + self, + original_func: cst.FunctionDef, + class_name: str, + entities: dict[str, str], + metadata: Mapping, + entity_keys: dict[str, list[str]] | None = None, + entity_init_params: dict[str, list[str]] | None = None, + live_vars: Mapping | None = None, + ): + resolver = EntityResolver( + class_name=class_name, + original_func=original_func, + entities=entities, + metadata=metadata, + entity_keys=entity_keys, + entity_init_params=entity_init_params, + ) + + # Pre-pass: rewrite send_async(...) into ctx.call_remote_async(...) + self.original_func = rewrite_send_async_in_function(original_func, resolver) + + self.ctx = SplitContext( + func_name=self.original_func.name.value, + class_name=class_name, + entities=entities, + resolver=resolver, + liveness=LivenessHelper(live_vars), + ) + + # Seed defined_vars with the function's parameters. + for param in self.original_func.params.params: + if param.name.value not in ("self", "ctx"): + self.ctx.defined_vars.add(param.name.value) + + def process(self) -> list[cst.FunctionDef]: + """Return [modified_root_func, *generated_step_funcs].""" + body = list(self.original_func.body.body) + new_body = self.ctx.split_body(body) + + modified = self.original_func.with_changes(body=cst.IndentedBlock(body=new_body)) + self.ctx.generated_functions.sort(key=lambda f: int(f.name.value.rsplit("_", 1)[-1])) + return [modified, *self.ctx.generated_functions] diff --git a/obol/src/obol/send_async.py b/obol/src/obol/send_async.py new file mode 100644 index 00000000..99738a94 --- /dev/null +++ b/obol/src/obol/send_async.py @@ -0,0 +1,127 @@ +"""Pre-pass that rewrites every `send_async()` statement in a +function body into a fire-and-forget `ctx.call_remote_async(...)` with a +a sink entry pushed in the reply_to stack indicating that returns should be supressed. +""" + +import libcst as cst + +from obol.cst_helpers import ( + call_remote_async_stmt, + params_with_reply_to, + sink_reply_to, +) +from obol.entity_resolver import EntityResolver + + +def is_send_async_stmt(stmt: cst.CSTNode) -> bool: + if not isinstance(stmt, cst.SimpleStatementLine) or not stmt.body: + return False + element = stmt.body[0] + if not isinstance(element, cst.Expr) or not isinstance(element.value, cst.Call): + return False + func = element.value.func + return isinstance(func, cst.Name) and func.value == "send_async" + + +def rewrite_send_async_in_function(func: cst.FunctionDef, resolver: EntityResolver) -> cst.FunctionDef: + """Return `func` with all send_async(...) statements rewritten.""" + rewriter = _Rewriter(resolver) + new_body = rewriter.rewrite_block(list(func.body.body)) + if not rewriter.touched: + return func + return func.with_changes(body=cst.IndentedBlock(body=new_body)) + + +class _Rewriter: + """In-place walker that replaces send_async() with ctx.call_remote_async(...)""" + + def __init__(self, resolver: EntityResolver): + self.resolver = resolver + self.touched = False + + def rewrite_block(self, body: list) -> list: + for i, stmt in enumerate(body): + if is_send_async_stmt(stmt): + body[i] = self._rewrite_stmt(stmt) + self.touched = True + elif isinstance(stmt, cst.If): + rebuilt = self._rewrite_if(stmt) + if rebuilt is not stmt: + body[i] = rebuilt + elif isinstance(stmt, (cst.For, cst.While)): + rebuilt = self._rewrite_loop(stmt) + if rebuilt is not stmt: + body[i] = rebuilt + return body + + def _rewrite_if(self, node: cst.If) -> cst.If: + before = self.touched + self.touched = False + new_body = self.rewrite_block(list(node.body.body)) + body_touched = self.touched + + new_orelse = node.orelse + orelse_touched = False + if node.orelse is not None: + if isinstance(node.orelse, cst.Else): + self.touched = False + new_else_body = self.rewrite_block(list(node.orelse.body.body)) + if self.touched: + new_orelse = node.orelse.with_changes(body=cst.IndentedBlock(body=new_else_body)) + orelse_touched = True + elif isinstance(node.orelse, cst.If): + rebuilt = self._rewrite_if(node.orelse) + if rebuilt is not node.orelse: + new_orelse = rebuilt + orelse_touched = True + + self.touched = before or body_touched or orelse_touched + if not (body_touched or orelse_touched): + return node + return node.with_changes( + body=cst.IndentedBlock(body=new_body), + orelse=new_orelse, + ) + + def _rewrite_loop(self, node): + before = self.touched + self.touched = False + new_body = self.rewrite_block(list(node.body.body)) + body_touched = self.touched + + new_orelse = node.orelse + orelse_touched = False + if node.orelse is not None and isinstance(node.orelse, cst.Else): + self.touched = False + new_else_body = self.rewrite_block(list(node.orelse.body.body)) + if self.touched: + new_orelse = node.orelse.with_changes(body=cst.IndentedBlock(body=new_else_body)) + orelse_touched = True + + self.touched = before or body_touched or orelse_touched + if not (body_touched or orelse_touched): + return node + return node.with_changes( + body=cst.IndentedBlock(body=new_body), + orelse=new_orelse, + ) + + def _rewrite_stmt(self, stmt: cst.SimpleStatementLine) -> cst.SimpleStatementLine: + inner_call = stmt.body[0].value.args[0].value # send_async() → + + if isinstance(inner_call.func, cst.Attribute): + receiver = inner_call.func.value + method = inner_call.func.attr.value + elif isinstance(inner_call.func, cst.Name): + receiver = inner_call.func + method = "insert" + else: + msg = f"Unsupported call type inside send_async: {type(inner_call.func)}" + raise ValueError(msg) + + key_value = self.resolver.key_for_call(receiver, inner_call, method) + op_name = self.resolver.operator_name_for(receiver) + + original_args = [arg.value for arg in inner_call.args] + params_value = params_with_reply_to(original_args, sink_reply_to()) + return call_remote_async_stmt(op_name, method, key_value, params_value) diff --git a/obol/src/obol/splitting/__init__.py b/obol/src/obol/splitting/__init__.py new file mode 100644 index 00000000..87daaf51 --- /dev/null +++ b/obol/src/obol/splitting/__init__.py @@ -0,0 +1,3 @@ +from obol.splitting.context import LoopContext, SplitContext + +__all__ = ["LoopContext", "SplitContext"] diff --git a/obol/src/obol/splitting/context.py b/obol/src/obol/splitting/context.py new file mode 100644 index 00000000..be0774ed --- /dev/null +++ b/obol/src/obol/splitting/context.py @@ -0,0 +1,208 @@ +"""Shared mutable state + the top-level dispatch loop for the splitter. + +`SplitContext` carries everything the per-construct handlers need to read and +mutate as they walk the function body: + + - the entity registry / class name / function name (for naming step funcs) + - counters (`split_counter`, `loop_iter_counter`) + - `defined_vars` — the live set of in-scope locals at the current point; + rebound at branch boundaries by handlers (so we hold it on the dataclass, + not by reference elsewhere) + - `generated_functions` — the list of step functions produced so far + - the `EntityResolver` and `LivenessHelper` adapters + - shared continuation-builder methods (`direct_continuation_call`, + `dispatch_block`, `restore_block`, `make_continuation`) + - `split_body` — the dispatch loop itself, called recursively from handlers +""" + +from typing import NamedTuple + +import libcst as cst + +from obol.cst_helpers import ( + CTX_KEY, + call_remote_async_stmt, + context_dict, + continuation_func, + continuation_params, + params_with_reply_to, + push_continuation_stmt, + resolve_context_stmt, + restore_tuple_from, +) +from obol.entity_resolver import EntityResolver +from obol.liveness import LivenessHelper +from obol.predicates import ( + ends_with_raise, + for_contains_remote_call, + if_contains_remote_call, + is_break, + is_continue, + is_gather, + while_contains_remote_call, +) + + +class LoopContext(NamedTuple): + """Carried while processing a loop body that contains splits. + + Tells nested handlers where `continue` and `break` should jump and what + state they need to ship along. + """ + + loop_step_name: str + op_name: str + iter_var_name: str | None + post_loop_func_name: str | None + # Vars to save when re-entering the loop step (continue + tail back-edges). + continue_save_vars: set[str] + # Vars to save when exiting to the post-loop step (break). None if no post-loop. + break_save_vars: set[str] | None + + +class SplitContext: + def __init__( + self, + func_name: str, + class_name: str, + entities: dict[str, str], + resolver: EntityResolver, + liveness: LivenessHelper, + ): + self.func_name = func_name + self.class_name = class_name + self.entities = entities + self.resolver = resolver + self.liveness = liveness + + self.split_counter = 1 + self.loop_iter_counter = 0 + self.defined_vars: set[str] = set() + self.generated_functions: list[cst.FunctionDef] = [] + + # ── helpers ─────────────────────────────────────────────── + + def next_step_name(self) -> str: + self.split_counter += 1 + return f"{self.func_name}_step_{self.split_counter}" + + def next_loop_iter_var(self) -> str: + self.loop_iter_counter += 1 + return f"__loop_index_{self.loop_iter_counter}" + + @property + def op_name(self) -> str: + return self.entities[self.class_name] + + # ── continuation builders ───────────────────────────────── + + def direct_continuation_call(self, func_name: str, vars_to_save: set | None = None) -> cst.SimpleStatementLine: + """ctx.call_remote_async to a continuation on this same entity, with + no reply_to push (used for loop back-edges, break/continue jumps, + post-loop entry, and the initial loop-step call). + """ + save_set = vars_to_save if vars_to_save is not None else self.defined_vars + sorted_vars = sorted(self.liveness.simple_name(v) for v in save_set) + ctx_dict = context_dict(sorted_vars) + params = continuation_params(ctx_dict, cst.Name("None"), cst.Name("reply_to")) + return call_remote_async_stmt(self.op_name, func_name, CTX_KEY, params) + + def dispatch_block( + self, + receiver: cst.BaseExpression, + method: str, + call_node: cst.Call, + next_func_name: str, + vars_to_save: set | None = None, + ) -> list: + """Build [push_continuation, call_remote_async] for a regular split. + + If `next_func_name == "None"`, no continuation push is emitted. + """ + key_value = self.resolver.key_for_call(receiver, call_node, method) + original_args = [arg.value for arg in call_node.args] + params_value = params_with_reply_to(original_args, cst.Name("reply_to")) + + target_op = self.resolver.operator_name_for(receiver) + async_call = call_remote_async_stmt(target_op, method, key_value, params_value) + + if next_func_name == "None": + return [async_call] + + save_set = vars_to_save if vars_to_save is not None else self.defined_vars + sorted_vars = sorted(self.liveness.simple_name(v) for v in save_set) + ctx_dict = context_dict(sorted_vars) + + push_call = push_continuation_stmt(self.op_name, next_func_name, ctx_dict) + return [push_call, async_call] + + def restore_block(self, vars_to_restore: set | None = None) -> list: + """params = resolve_context(...); (v1, v2, ...) = (params.get('v1'), ...).""" + vars_source = vars_to_restore if vars_to_restore is not None else self.defined_vars + sorted_vars = sorted(self.liveness.simple_name(v) for v in vars_source) + if not sorted_vars: + return [] + return [resolve_context_stmt(), restore_tuple_from("params", sorted_vars)] + + def make_continuation(self, name: str, body: list, target_var: str) -> cst.FunctionDef: + return continuation_func(name, body, target_var, self.op_name) + + def track_vars(self, stmt: cst.CSTNode) -> None: + """Add any variables assigned (recursively) within stmt to defined_vars.""" + self.defined_vars.update(LivenessHelper.collect_assigned_vars([stmt])) + + # ── top-level dispatch loop ────────────────────────────────────── + + def split_body(self, body: list, loop_context: LoopContext | None = None) -> list: + # Imported lazily to break the import cycle + from obol.splitting.gather import handle_gather # noqa: PLC0415 + from obol.splitting.if_split import handle_if # noqa: PLC0415 + from obol.splitting.loop import handle_loop # noqa: PLC0415 + from obol.splitting.remote_call import handle_remote_call # noqa: PLC0415 + + for i, stmt in enumerate(body): + # gather(...) → fan-out + barrier join + if is_gather(stmt): + return handle_gather(self, body, i, loop_context) + + # continue → jump back to loop step + if is_continue(stmt) and loop_context: + vars_to_save = loop_context.continue_save_vars & self.defined_vars + self.liveness.add_synthetic_loop_vars(vars_to_save, self.defined_vars) + direct = self.direct_continuation_call(loop_context.loop_step_name, vars_to_save) + return [*body[:i], direct] + + # break → jump to post-loop continuation (or return if none) + if is_break(stmt) and loop_context: + if loop_context.post_loop_func_name and loop_context.break_save_vars is not None: + vars_to_save = loop_context.break_save_vars & self.defined_vars + self.liveness.add_synthetic_loop_vars(vars_to_save, self.defined_vars) + jump = self.direct_continuation_call(loop_context.post_loop_func_name, vars_to_save) + else: + jump = cst.SimpleStatementLine(body=[cst.Return(value=None)]) + return [*body[:i], jump] + + if self.resolver.is_remote_call(stmt): + return handle_remote_call(self, body, i, loop_context) + + if isinstance(stmt, cst.If) and if_contains_remote_call(self.resolver, stmt, loop_context): + return handle_if(self, body, i, loop_context) + + if isinstance(stmt, cst.For) and for_contains_remote_call(self.resolver, stmt): + return handle_loop(self, body, i, loop_context) + + if isinstance(stmt, cst.While) and while_contains_remote_call(self.resolver, stmt): + return handle_loop(self, body, i, loop_context) + + self.track_vars(stmt) + + # No remote calls found — tail of a loop body or normal function end. + if loop_context: + if body and ends_with_raise(body): + return body + # Tail back to loop step, pass live-in at loop header. + vars_to_save = loop_context.continue_save_vars & self.defined_vars + self.liveness.add_synthetic_loop_vars(vars_to_save, self.defined_vars) + direct_call = self.direct_continuation_call(loop_context.loop_step_name, vars_to_save) + return [*body, direct_call] + return body diff --git a/obol/src/obol/splitting/gather.py b/obol/src/obol/splitting/gather.py new file mode 100644 index 00000000..13cfbb73 --- /dev/null +++ b/obol/src/obol/splitting/gather.py @@ -0,0 +1,289 @@ +"""Splitting at `gather(...)` calls — fan-out N parallel dispatches into a join step. + +Two types of gather supported: + - static: `(a, b) = gather(x.foo(), y.bar())` — we can determine the number of parallel calls at compile time, so we + send N calls tagged by static indices and a barrier sized to N. + + - dynamic: `gather(*[e.bar() for e in xs])` — comprehensions with a single for components and currently + NO FILTERS ARE SUPPORTED!!. Number of parallel calls is determined at runtimes. + +Both use the sa,e join continuation that update_gather_barrier helpers. +""" + +import libcst as cst + +from obol.cst_helpers import ( + CTX_KEY, + assign_stmt, + call_remote_async_stmt, + context_dict, + dict_from_pairs, + init_gather_barrier_stmt, + params_with_reply_to, + reply_entry, + restore_tuple_from, +) +from obol.splitting.context import LoopContext, SplitContext + + +def handle_gather(ctx: SplitContext, body: list, i: int, loop_context: LoopContext | None = None) -> list: + """Split at a `gather(...)` call: fan-out + barrier join.""" + stmt = body[i] + post_split = body[i + 1 :] + + target_name, tuple_target, gather_call = _extract_gather_target(stmt) + + # Dynamic case: `gather(*)` + spread_comp: cst.ListComp | cst.SetComp | cst.GeneratorExp | None = None + if ( + len(gather_call.args) == 1 + and gather_call.args[0].star == "*" + and isinstance(gather_call.args[0].value, (cst.ListComp, cst.SetComp, cst.GeneratorExp)) + ): + spread_comp = gather_call.args[0].value + + if spread_comp is None: + gather_args = [arg.value for arg in gather_call.args] + if not gather_args: + msg = "gather() requires at least one call argument" + raise ValueError(msg) + + # Snapshot of locals to save across the gather. Take a COPY so that later + # mutations to ctx.defined_vars (adding gather targets) don't leak into + # the dispatch block / restore block. + vars_to_save = set(ctx.liveness.vars_to_save_at(stmt, ctx.defined_vars)) + + join_name = ctx.next_step_name() + + # Build dispatch before adding gather targets to defined_vars. + if spread_comp is not None: + dispatch_block = _build_spread_dispatch(ctx, spread_comp, join_name, vars_to_save) + else: + dispatch_block = _build_static_dispatch(ctx, gather_args, join_name, vars_to_save) + + # The gather's targets become defined AFTER the join step resumes — + # add them now so post-gather processing (inside join body) tracks them. + if target_name is not None: + ctx.defined_vars.add(target_name) + if tuple_target is not None: + for el in tuple_target.elements: + if isinstance(el.value, cst.Name): + ctx.defined_vars.add(el.value.value) + + join_body = _build_join_body(ctx, target_name, tuple_target, vars_to_save, post_split, loop_context) + join_func = ctx.make_continuation(join_name, join_body, "_gather_partial") + ctx.generated_functions.append(join_func) + + return body[:i] + dispatch_block + + +# ── helpers ──────────────────────────────────────────────────────────── + + +def _extract_gather_target( + stmt: cst.SimpleStatementLine, +) -> tuple[str | None, cst.Tuple | None, cst.Call]: + """Pull out (target_name, tuple_target, gather_call) from the stmt's element.""" + element = stmt.body[0] + tuple_target: cst.Tuple | None = None + target_name: str | None = None + + if isinstance(element, cst.Assign): + target = element.targets[0].target + if isinstance(target, cst.Name): + target_name = target.value + elif isinstance(target, cst.Tuple): + tuple_target = target + else: + msg = f"gather() result must be a Name or Tuple, got {type(target).__name__}" + raise ValueError(msg) + return target_name, tuple_target, element.value + + if isinstance(element, cst.Expr): + return None, None, element.value + + msg = f"Unsupported gather statement element: {type(element).__name__}" + raise ValueError(msg) + + +def _resolve_arg_call(call_node: cst.CSTNode, ctx_label: str) -> tuple[cst.BaseExpression, str]: + """Validate a gather argument call shape and return (receiver, method). + + Constructor calls map to method 'insert'; method calls return the attr name. + """ + if not isinstance(call_node, cst.Call): + msg = f"{ctx_label} arguments must be method calls, got {type(call_node).__name__}" + raise ValueError(msg) + if isinstance(call_node.func, cst.Name): + return call_node.func, "insert" + if isinstance(call_node.func, cst.Attribute): + return call_node.func.value, call_node.func.attr.value + msg = f"Unsupported {ctx_label} argument call type: {type(call_node.func).__name__}" + raise ValueError(msg) + + +def _validate_entity_call(receiver: cst.BaseExpression, method: str, entities: dict, ctx_label: str) -> None: + if method == "insert" and isinstance(receiver, cst.Name) and receiver.value not in entities: + msg = f"{ctx_label} elements must be entity methods or constructors, got plain function '{receiver.value}'" + raise ValueError(msg) + + +def _build_static_dispatch(ctx: SplitContext, gather_args: list, join_name: str, vars_to_save: set | None) -> list: + reply_op_name = ctx.op_name + save_set = vars_to_save if vars_to_save is not None else ctx.defined_vars + sorted_vars = sorted(ctx.liveness.simple_name(v) for v in save_set) + saved_dict = context_dict(sorted_vars) + + statements: list = [init_gather_barrier_stmt(cst.Integer(str(len(gather_args))), saved_dict)] + + for tag, call_node in enumerate(gather_args): + receiver, method = _resolve_arg_call(call_node, "gather()") + _validate_entity_call(receiver, method, ctx.entities, "gather()") + target_op = ctx.resolver.operator_name_for(receiver) + key_value = ctx.resolver.key_for_call(receiver, call_node, method) + + join_context = dict_from_pairs( + [ + ("_g_barrier", cst.Name("_gather_id")), + ("_g_tag", cst.Integer(str(tag))), + ] + ) + child_reply_list = cst.List( + elements=[cst.Element(value=reply_entry(reply_op_name, join_name, CTX_KEY, join_context))] + ) + + reply_to_var = f"_g_reply_{tag}" + reply_assign = assign_stmt(cst.Name(reply_to_var), child_reply_list) + + original_args = [arg.value for arg in call_node.args] + params_value = params_with_reply_to(original_args, cst.Name(reply_to_var)) + async_call = call_remote_async_stmt(target_op, method, key_value, params_value) + + statements.extend([reply_assign, async_call]) + + return statements + + +def _build_spread_dispatch( + ctx: SplitContext, + comp: cst.ListComp | cst.SetComp | cst.GeneratorExp, + join_name: str, + vars_to_save: set | None, +) -> list: + if comp.for_in.inner_for_in is not None or comp.for_in.ifs: + # TODO: Add support for filters and multiple for-clauses in comprehensions. + msg = "gather(*) currently only supports a single, unfiltered for-clause" + raise ValueError(msg) + + call_node = comp.elt + receiver, method = _resolve_arg_call(call_node, "gather(*)") + _validate_entity_call(receiver, method, ctx.entities, "gather(*)") + + target_op = ctx.resolver.operator_name_for(receiver) + key_value = ctx.resolver.key_for_call(receiver, call_node, method) + + reply_op_name = ctx.op_name + save_set = vars_to_save if vars_to_save is not None else ctx.defined_vars + sorted_vars = sorted(ctx.liveness.simple_name(v) for v in save_set) + saved_dict = context_dict(sorted_vars) + + # _g_iter = list() — materialize so we can take len() and iterate + # again. Covers enumerate/zip/range/generators safely. + materialize_stmt = assign_stmt( + cst.Name("_g_iter"), + cst.Call(func=cst.Name("list"), args=[cst.Arg(value=comp.for_in.iter)]), + ) + init_stmt = init_gather_barrier_stmt( + cst.Call(func=cst.Name("len"), args=[cst.Arg(value=cst.Name("_g_iter"))]), + saved_dict, + ) + + # for _g_tag, in enumerate(_g_iter): + # _g_reply = [{...join entry...}]; ctx.call_remote_async(...) + # Tuple targets like need parentheses so that they match with enumerate. + comp_target = comp.for_in.target + if isinstance(comp_target, cst.Tuple) and not comp_target.lpar: + comp_target = comp_target.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) + loop_target = cst.Tuple(elements=[cst.Element(value=cst.Name("_g_tag")), cst.Element(value=comp_target)]) + loop_iter = cst.Call(func=cst.Name("enumerate"), args=[cst.Arg(value=cst.Name("_g_iter"))]) + + join_context = dict_from_pairs( + [ + ("_g_barrier", cst.Name("_gather_id")), + ("_g_tag", cst.Name("_g_tag")), + ] + ) + reply_assign = assign_stmt( + cst.Name("_g_reply"), + cst.List(elements=[cst.Element(value=reply_entry(reply_op_name, join_name, CTX_KEY, join_context))]), + ) + + original_args = [arg.value for arg in call_node.args] + params_value = params_with_reply_to(original_args, cst.Name("_g_reply")) + async_call = call_remote_async_stmt(target_op, method, key_value, params_value) + + dispatch_loop = cst.For( + target=loop_target, + iter=loop_iter, + body=cst.IndentedBlock(body=[reply_assign, async_call]), + ) + + # Empty iterable → zero dispatches → nothing would ever call + # update_gather_barrier, so the join (and thus the reply) never fires. + # Fire a single self-call to the join; the total==0 barrier resolves it + # immediately with empty results. + empty_context = dict_from_pairs([("_g_barrier", cst.Name("_gather_id")), ("_g_tag", cst.Integer("0"))]) + empty_call = call_remote_async_stmt( + reply_op_name, + join_name, + CTX_KEY, + params_with_reply_to([empty_context, cst.Name("None")], cst.Name("None")), + ) + is_empty = cst.Comparison( + left=cst.Call(func=cst.Name("len"), args=[cst.Arg(value=cst.Name("_g_iter"))]), + comparisons=[cst.ComparisonTarget(operator=cst.Equal(), comparator=cst.Integer("0"))], + ) + guard = cst.If( + test=is_empty, + body=cst.IndentedBlock(body=[empty_call]), + orelse=cst.Else(body=cst.IndentedBlock(body=[dispatch_loop])), + ) + return [materialize_stmt, init_stmt, guard] + + +def _build_join_body( + ctx: SplitContext, + target_name: str | None, + tuple_target: cst.Tuple | None, + vars_to_save: set | None, + post_split: list, + loop_context: LoopContext | None, +) -> list: + """Body of the gather join continuation.""" + save_set = vars_to_save if vars_to_save is not None else ctx.defined_vars + sorted_vars = sorted(ctx.liveness.simple_name(v) for v in save_set) + + body_stmts: list = [ + cst.parse_statement("barrier_id = func_context['_g_barrier']"), + cst.parse_statement("_g_tag = func_context['_g_tag']"), + cst.parse_statement( + "(is_complete, _g_results, saved, parent_reply_to) = " + "update_gather_barrier(ctx, barrier_id, _g_tag, _gather_partial)" + ), + cst.parse_statement("if not is_complete:\n return\n"), + ] + + # Restore locals from `saved` dict (direct .get() — no resolve_context here). + if sorted_vars: + body_stmts.append(restore_tuple_from("saved", sorted_vars)) + + body_stmts.append(cst.parse_statement("reply_to = parent_reply_to")) + + bind_target: cst.BaseExpression | None = tuple_target or ( + cst.Name(target_name) if target_name is not None else None + ) + if bind_target is not None: + body_stmts.append(assign_stmt(bind_target, cst.Name("_g_results"))) + + body_stmts.extend(ctx.split_body(list(post_split), loop_context)) + return body_stmts diff --git a/obol/src/obol/splitting/if_split.py b/obol/src/obol/splitting/if_split.py new file mode 100644 index 00000000..0f58f75d --- /dev/null +++ b/obol/src/obol/splitting/if_split.py @@ -0,0 +1,75 @@ +"""Splitting at an if-statement whose branches contain remote calls. + +The if/elif/else structure is preserved, but each branch's body is recursively +split. When the if-branch dispatches but there's no else, the post-if statements +are folded into a synthetic else to prevent fall-through. When inside a loop, a missing else with +no post-if becomes an explicit loop-back jump. +""" + +import libcst as cst + +from obol.predicates import any_remote_call, ends_with_terminator +from obol.splitting.context import LoopContext, SplitContext + + +def handle_if(ctx: SplitContext, body: list, i: int, loop_context: LoopContext | None = None) -> list: + """Split at an if-statement (at index i) whose branches contain remote calls.""" + pre_if = body[:i] + result = _process_if_node(ctx, body[i], body[i + 1 :], loop_context) + return pre_if + result + + +def _process_if_node( + ctx: SplitContext, + if_stmt: cst.If, + post_if: list, + loop_context: LoopContext | None = None, +) -> list: + """Recursively process an if/elif node. Returns a list of statements.""" + if_body_stmts = list(if_stmt.body.body) + if_branch_dispatches = any_remote_call(ctx.resolver, if_body_stmts, loop_context) + + # Snapshot before branching — each branch starts from the same defined_vars. + # This way variables defined in one branch aren't considered live in the other. + saved_vars = ctx.defined_vars.copy() + + # If a branch already ends in `return`/`raise`, post_if is dead code. + if_tail = [] if ends_with_terminator(if_body_stmts) else post_if + new_if_body = ctx.split_body(if_body_stmts + if_tail, loop_context) + + # Restore for else/elif branch. + ctx.defined_vars = saved_vars.copy() + + new_else: cst.Else | cst.If | None = None + if if_stmt.orelse is not None: + if isinstance(if_stmt.orelse, cst.Else): + else_body_stmts = list(if_stmt.orelse.body.body) + else_tail = [] if ends_with_terminator(else_body_stmts) else post_if + new_else_body = ctx.split_body(else_body_stmts + else_tail, loop_context) + new_else = cst.Else(body=cst.IndentedBlock(body=new_else_body)) + elif isinstance(if_stmt.orelse, cst.If): + # elif chain + elif_result = _process_if_node(ctx, if_stmt.orelse, post_if, loop_context) + if len(elif_result) == 1 and isinstance(elif_result[0], cst.If): + new_else = elif_result[0] + else: + new_else = cst.Else(body=cst.IndentedBlock(body=elif_result)) + elif if_branch_dispatches and post_if: + # No else but if-branch dispatches — fold post-if into else to prevent fallthrough. + processed_post_if = ctx.split_body(post_if, loop_context) + new_else = cst.Else(body=cst.IndentedBlock(body=processed_post_if)) + elif if_branch_dispatches and not post_if and loop_context: + # No else, no post-if, inside a loop — the false path must loop back. + vars_to_save = loop_context.continue_save_vars & ctx.defined_vars + ctx.liveness.add_synthetic_loop_vars(vars_to_save, ctx.defined_vars) + loop_back = ctx.direct_continuation_call(loop_context.loop_step_name, vars_to_save) + new_else = cst.Else(body=cst.IndentedBlock(body=[loop_back])) + + # Restore to pre-branch state — the caller decides what's defined after the if. + ctx.defined_vars = saved_vars + + new_if = if_stmt.with_changes(body=cst.IndentedBlock(body=new_if_body), orelse=new_else) + + if if_stmt.orelse is not None or (if_branch_dispatches and (post_if or loop_context)): + return [new_if] + return [new_if, *post_if] diff --git a/obol/src/obol/splitting/loop.py b/obol/src/obol/splitting/loop.py new file mode 100644 index 00000000..9957e11b --- /dev/null +++ b/obol/src/obol/splitting/loop.py @@ -0,0 +1,266 @@ +"""Splitting at a for/while-loop whose body contains remote calls. + +Loops are turned into three functions: the pre-loop code, +a loop step, and the post-loop code. The loop step is an if/else statement +that checks the loop bounds/test and either dispatches the body or jumps to the post-loop continuation. +NOTE we need a post-loop continuation for break statements +NOTE while loops that have remote calls in the condition are translated to while True with an if condition: break + +Four iterator shapes are currently supported for `for`: + - `for x in range(n)` and `for x in range(start, stop)` → bounds via range args + - `for a, b in zip(xs, ys)` → bound = min(len(xs), len(ys)); per-iteration assigns a = xs[i], b = ys[i] + - `for x in items` → bound = len(items); x = items[i] + - `for i, x in enumerate(items)` → bound = len(items); i = idx[+start]; x = items[idx] +""" + +import libcst as cst + +from obol.cst_helpers import assign_stmt +from obol.liveness import LivenessHelper +from obol.splitting.context import LoopContext, SplitContext + + +def handle_loop(ctx: SplitContext, body: list, i: int, loop_context: LoopContext | None = None) -> list: + """Split at a for/while-loop whose body contains remote calls.""" + loop_stmt = body[i] + pre_loop = body[:i] + post_loop = body[i + 1 :] + + is_for = isinstance(loop_stmt, cst.For) + loop_step_name = ctx.next_step_name() + + # For-loop initialization (None for While). + init_iter = None + var_assigns: list | None = None + inc_idx = None + loop_var_name = "_loop_var" + extra_loop_vars: list[str] = [] + bound_expr = None + state_idx_access = None + + if is_for: + iter_var_name = ctx.next_loop_iter_var() + state_idx_access = cst.Name(iter_var_name) + init_iter, var_assigns, inc_idx, bound_expr, loop_var_name, extra_loop_vars = _build_for_iter( + ctx, loop_stmt, iter_var_name, state_idx_access + ) + + # Pre-scan loop body assignments — these may flow into the loop step via + # the back-edge (in addition to anything from outside the loop). + loop_body_stmts = list(loop_stmt.body.body) + loop_body_vars = LivenessHelper.collect_assigned_vars(loop_body_stmts) + + known_vars = set(ctx.defined_vars) | loop_body_vars + + # Live-in at loop entry: what's needed to (re-)evaluate iter/test and body. + live_in = ctx.liveness.live_in_at_loop(loop_stmt) if isinstance(loop_stmt, (cst.For, cst.While)) else None + if live_in is not None: + entry_save_vars = live_in & known_vars + ctx.liveness.add_synthetic_loop_vars(entry_save_vars, ctx.defined_vars) + else: + entry_save_vars = set(known_vars) + + # Live-out of the loop: an upper bound on what the post-loop code needs. + live_out = ctx.liveness.live_out_at_loop(loop_stmt) if isinstance(loop_stmt, (cst.For, cst.While)) else None + if live_out is not None: + post_loop_save_vars = live_out & known_vars + ctx.liveness.add_synthetic_loop_vars(post_loop_save_vars, ctx.defined_vars) + else: + post_loop_save_vars = set(known_vars) + + # Initial entry into the loop: only the outside knows about pre-loop vars. + vars_for_loop_entry = entry_save_vars & ctx.defined_vars + ctx.liveness.add_synthetic_loop_vars(vars_for_loop_entry, ctx.defined_vars) + direct_call = ctx.direct_continuation_call(loop_step_name, vars_for_loop_entry) + + # Restore at the start of the loop step covers everything any incoming edge + # (initial entry or body back-edge) might pass. + restore_block = ctx.restore_block(entry_save_vars) + + saved_vars = ctx.defined_vars.copy() + + # Post-loop code: processed with the OUTER loop_context (not this loop's). + if post_loop: + post_loop_func_name = ctx.next_step_name() + post_loop_body = ctx.split_body(post_loop, loop_context) + post_loop_restore = ctx.restore_block(post_loop_save_vars) + post_loop_func = ctx.make_continuation( + post_loop_func_name, post_loop_restore + post_loop_body, "placeholder_return" + ) + ctx.generated_functions.append(post_loop_func) + exit_stmts = [ctx.direct_continuation_call(post_loop_func_name, post_loop_save_vars)] + else: + post_loop_func_name = None + exit_stmts = [cst.SimpleStatementLine(body=[cst.Return(value=None)])] + + # Restore before processing loop body (independent path). + ctx.defined_vars = saved_vars.copy() + + if is_for and loop_var_name != "_loop_var": + ctx.defined_vars.add(loop_var_name) + for v in extra_loop_vars: # additional zip tuple-target variables + ctx.defined_vars.add(v) + + inner_loop_context = LoopContext( + loop_step_name=loop_step_name, + op_name=ctx.op_name, + iter_var_name=state_idx_access.value if is_for else None, + post_loop_func_name=post_loop_func_name, + continue_save_vars=entry_save_vars, + break_save_vars=post_loop_save_vars if post_loop else None, + ) + loop_body_processed = ctx.split_body(loop_body_stmts, inner_loop_context) + + # Restore to pre-branch state. + ctx.defined_vars = saved_vars + + if is_for: + # if __loop_index >= bound: ; else: ; ; + test_cond = cst.Comparison( + left=state_idx_access, + comparisons=[cst.ComparisonTarget(operator=cst.GreaterThanEqual(), comparator=bound_expr)], + ) + body_block = cst.IndentedBlock(body=exit_stmts) + else_block = cst.Else(body=cst.IndentedBlock(body=[*(var_assigns or []), inc_idx, *loop_body_processed])) + if_block = cst.If(test=test_cond, body=body_block, orelse=else_block) + loop_step_body = [*restore_block, if_block] + else: + test_cond = loop_stmt.test + is_while_true = isinstance(test_cond, cst.Name) and test_cond.value == "True" + if is_while_true: + # `while True:` — the body itself handles exit via break. + loop_step_body = [*restore_block, *loop_body_processed] + else: + body_block = cst.IndentedBlock(body=loop_body_processed) + else_block = cst.Else(body=cst.IndentedBlock(body=exit_stmts)) + if_block = cst.If(test=test_cond, body=body_block, orelse=else_block) + loop_step_body = [*restore_block, if_block] + + cont_func = ctx.make_continuation(loop_step_name, loop_step_body, "placeholder_return") + ctx.generated_functions.append(cont_func) + + if is_for: + return [*pre_loop, init_iter, direct_call] + return [*pre_loop, direct_call] + + +# ── for-iterator setup ───────────────────────────────────────────────── + + +def _build_for_iter( + ctx: SplitContext, + loop_stmt: cst.For, + iter_var_name: str, + state_idx_access: cst.Name, +) -> tuple[ + cst.SimpleStatementLine, # init_iter: __loop_index = + list, # var_assigns: per-iteration var bindings + cst.SimpleStatementLine, # inc_idx: __loop_index += 1 + cst.BaseExpression, # bound_expr: stop value for the bounds check + str, # loop_var_name (or "_loop_var" if no name target) + list[str], # extra_loop_vars (zip tuple targets after the first) +]: + iter_node = loop_stmt.iter + is_call = isinstance(iter_node, cst.Call) and isinstance(iter_node.func, cst.Name) + is_zip_iter = is_call and iter_node.func.value == "zip" + is_enumerate_iter = is_call and iter_node.func.value == "enumerate" + + if is_enumerate_iter: + # for i, x in enumerate(items[, start]): bound = len(items); i = idx[+start]; x = items[idx] + enum_args = [arg.value for arg in iter_node.args] + iterable = enum_args[0] + start = enum_args[1] if len(enum_args) >= 2 else None + bound_expr = cst.Call(func=cst.Name("len"), args=[cst.Arg(value=iterable)]) + init_iter = assign_stmt(cst.Name(iter_var_name), cst.Integer("0")) + ctx.defined_vars.add(iter_var_name) + + target = loop_stmt.target + if not isinstance(target, cst.Tuple) or len(target.elements) != 2: + msg = "enumerate() loop target must unpack to two names, e.g. `for i, x in enumerate(items):`" + raise ValueError(msg) + idx_name, val_name = (el.value.value for el in target.elements) + idx_value = ( + state_idx_access + if start is None + else cst.BinaryOperation(left=state_idx_access, operator=cst.Add(), right=start) + ) + var_assigns = [ + assign_stmt(cst.Name(idx_name), idx_value), + assign_stmt( + cst.Name(val_name), + cst.Subscript( + value=iterable, + slice=[cst.SubscriptElement(slice=cst.Index(value=state_idx_access))], + ), + ), + ] + loop_var_name, extra_loop_vars = idx_name, [val_name] + elif is_zip_iter: + # for a, b in zip(xs, ys): bound = min(len(xs), len(ys)); a = xs[i]; b = ys[i] + zip_args = [arg.value for arg in iter_node.args] + len_calls = [cst.Call(func=cst.Name("len"), args=[cst.Arg(value=arg)]) for arg in zip_args] + if len(len_calls) == 1: + bound_expr = len_calls[0] + else: + bound_expr = cst.Call(func=cst.Name("min"), args=[cst.Arg(value=lc) for lc in len_calls]) + + init_iter = assign_stmt(cst.Name(iter_var_name), cst.Integer("0")) + ctx.defined_vars.add(iter_var_name) + + target = loop_stmt.target + if isinstance(target, cst.Tuple): + target_names = [elem.value.value for elem in target.elements if isinstance(elem.value, cst.Name)] + elif isinstance(target, cst.Name): + target_names = [target.value] + else: + target_names = [] + + var_assigns = [ + assign_stmt( + cst.Name(var_name), + cst.Subscript( + value=zip_arg, + slice=[cst.SubscriptElement(slice=cst.Index(value=state_idx_access))], + ), + ) + for zip_arg, var_name in zip(zip_args, target_names, strict=False) + ] + + loop_var_name = target_names[0] if target_names else "_loop_var" + extra_loop_vars = target_names[1:] + else: + start_expr, bound_expr, is_range = _parse_loop_iter(iter_node) + init_iter = assign_stmt(cst.Name(iter_var_name), start_expr) + ctx.defined_vars.add(iter_var_name) + + loop_var_name = "_loop_var" + if isinstance(loop_stmt.target, cst.Name): + loop_var_name = loop_stmt.target.value + + if is_range: + var_val = state_idx_access + else: + var_val = cst.Subscript( + value=iter_node, + slice=[cst.SubscriptElement(slice=cst.Index(value=state_idx_access))], + ) + var_assigns = [assign_stmt(cst.Name(loop_var_name), var_val)] + extra_loop_vars = [] + + inc_idx = cst.SimpleStatementLine( + body=[cst.AugAssign(target=state_idx_access, operator=cst.AddAssign(), value=cst.Integer("1"))] + ) + + return init_iter, var_assigns, inc_idx, bound_expr, loop_var_name, extra_loop_vars + + +def _parse_loop_iter(iter_node: cst.BaseExpression) -> tuple[cst.BaseExpression, cst.BaseExpression, bool]: + """Return (start, bound, is_range). Supports range() and collection iteration.""" + if isinstance(iter_node, cst.Call) and isinstance(iter_node.func, cst.Name) and iter_node.func.value == "range": + if len(iter_node.args) == 1: + return cst.Integer("0"), iter_node.args[0].value, True + if len(iter_node.args) >= 2: + return iter_node.args[0].value, iter_node.args[1].value, True + + bound = cst.Call(func=cst.Name("len"), args=[cst.Arg(value=iter_node)]) + return cst.Integer("0"), bound, False diff --git a/obol/src/obol/splitting/remote_call.py b/obol/src/obol/splitting/remote_call.py new file mode 100644 index 00000000..cb44ce61 --- /dev/null +++ b/obol/src/obol/splitting/remote_call.py @@ -0,0 +1,102 @@ +"""Splitting at a single remote call statement. + +Each dispatch is a pair of statements: + reply_to = push_continuation(...) + ctx.call_remote_async(...) + +In the post-split body we use another helper to restore live variables from the context dict: + params = resolve_context(...) +""" + +import libcst as cst + +from obol.cst_helpers import assign_stmt +from obol.splitting.context import LoopContext, SplitContext + + +def handle_remote_call(ctx: SplitContext, body: list, i: int, loop_context: LoopContext | None = None) -> list: + stmt = body[i] + post_split = body[i + 1 :] + has_continuation = len(post_split) > 0 + + target_var, call_node, receiver, remote_method, tuple_target = extract_call_info(stmt) + + # Tail-call optimization: if the only follow-up is `return `, + # skip the continuation — the reply already flows back via reply_to. + if ( + len(post_split) == 1 + and isinstance(post_split[0], cst.SimpleStatementLine) + and len(post_split[0].body) == 1 + and isinstance(post_split[0].body[0], cst.Return) + ): + ret_stmt = post_split[0].body[0] + if isinstance(ret_stmt.value, cst.Name) and ret_stmt.value.value == target_var: + has_continuation = False + + vars_to_save = ctx.liveness.vars_to_save_at(stmt, ctx.defined_vars) + + if has_continuation: + next_func_name = ctx.next_step_name() + + dispatch_block = ctx.dispatch_block(receiver, remote_method, call_node, next_func_name, vars_to_save) + restore_block = ctx.restore_block(vars_to_save) + + # Tuple unpacking continuation: inject `(a, b) = __tuple_result` and + # add each element to defined_vars so the rest of the body knows them. + unpack_stmts = [] + if tuple_target is not None: + for el in tuple_target.elements: + if isinstance(el.value, cst.Name): + ctx.defined_vars.add(el.value.value) + unpack_stmts.append(assign_stmt(tuple_target, cst.Name(target_var))) + elif target_var != "placeholder_return": + ctx.defined_vars.add(target_var) + + cont_body = restore_block + unpack_stmts + ctx.split_body(post_split, loop_context) + cont_func = ctx.make_continuation(next_func_name, cont_body, target_var) + ctx.generated_functions.append(cont_func) + + return body[:i] + dispatch_block + + # Last remote call — either jump back to a loop step or fall through. + next_target = loop_context.loop_step_name if loop_context else "None" + dispatch_block = ctx.dispatch_block(receiver, remote_method, call_node, next_target, vars_to_save) + return body[:i] + dispatch_block + + +def extract_call_info( + stmt: cst.SimpleStatementLine, +) -> tuple[str, cst.Call, cst.BaseExpression, str, cst.Tuple | None]: + """Decompose ` = .(args)` (or its variants). + + Returns (target_var, call_node, receiver, method, tuple_target). + """ + element = stmt.body[0] + tuple_target: cst.Tuple | None = None + + if isinstance(element, cst.Assign): + target = element.targets[0].target + if isinstance(target, cst.Tuple): + target_var = "__tuple_result" + tuple_target = target + else: + target_var = target.value + call_node = element.value + elif isinstance(element, cst.Expr): + target_var = "placeholder_return" + call_node = element.value + else: + msg = f"Unexpected element: {type(element)}" + raise ValueError(msg) + + if isinstance(call_node.func, cst.Name): + receiver = call_node.func + remote_method = "insert" + elif isinstance(call_node.func, cst.Attribute): + receiver = call_node.func.value + remote_method = call_node.func.attr.value + else: + msg = f"Unsupported call type: {type(call_node.func)}" + raise ValueError(msg) + + return target_var, call_node, receiver, remote_method, tuple_target diff --git a/obol/src/obol/transformers/__init__.py b/obol/src/obol/transformers/__init__.py new file mode 100644 index 00000000..f10fe07a --- /dev/null +++ b/obol/src/obol/transformers/__init__.py @@ -0,0 +1,15 @@ +from .annotations import EntityTypeReplacer +from .linearize import RemoteCallLinearizer +from .normalize import normalize_function_body +from .return_handler import ReturnHandlerTransformer +from .short_circuit import ShortCircuitRewriter +from .state_access import StateAccessTransformer + +__all__ = [ + "EntityTypeReplacer", + "RemoteCallLinearizer", + "ReturnHandlerTransformer", + "ShortCircuitRewriter", + "StateAccessTransformer", + "normalize_function_body", +] diff --git a/obol/src/obol/transformers/annotations.py b/obol/src/obol/transformers/annotations.py new file mode 100644 index 00000000..ccafd36d --- /dev/null +++ b/obol/src/obol/transformers/annotations.py @@ -0,0 +1,65 @@ +"""Rewrite type annotations that reference entities to their key types, e.g. `item: Item` -> `item: str`.""" + +import libcst as cst + + +class AnnotationNameReplacer(cst.CSTTransformer): + def __init__(self, get_key_type_func): + super().__init__() + self.get_key_type = get_key_type_func + + def leave_Name(self, original_node, updated_node): + replacement = self.get_key_type(original_node.value) + if replacement: + return updated_node.with_changes(value=replacement) + return updated_node + + +class EntityTypeReplacer(cst.CSTTransformer): + """ + Replaces entity type references in annotations with the key's type. + e.g., `item: Item` -> `item: str`, `-> Item` -> `-> str` + Also handles: `items: list[Item]` -> `items: list[str]`, `list[list[Item]]` -> `list[list[str]]` + """ + + def __init__( + self, + entity_keys: dict[str, str], + entity_init_params: dict[str, dict[str, str]], + entity_key_types: dict[str, str] | None = None, + ): + super().__init__() + self.entity_keys = entity_keys + self.entity_init_params = entity_init_params + self.entity_key_types = entity_key_types or {} + + def _get_key_type(self, entity_name: str): + """Resolve an entity name to its key's type string, or None.""" + # 1. Check if we explicitly found a return type for __key__ + if entity_name in self.entity_key_types: + return self.entity_key_types[entity_name] + + # 2. Check if the key field is in __init__ params + key_fields = self.entity_keys.get(entity_name) + init_params = self.entity_init_params.get(entity_name) + + if key_fields and isinstance(key_fields, list): + if len(key_fields) > 1: + # Composite keys are concatenated into strings + return "str" + + # Single key: use its original type + key_field = key_fields[0] + if init_params and key_field in init_params: + return init_params[key_field] + + # 3. Fallback: if it's a known entity, default to str + if entity_name in self.entity_keys: + return "str" + + return None + + def leave_Annotation(self, _original_node, updated_node): + replacer = AnnotationNameReplacer(self._get_key_type) + new_ann = updated_node.annotation.visit(replacer) + return updated_node.with_changes(annotation=new_ann) diff --git a/obol/src/obol/transformers/linearize.py b/obol/src/obol/transformers/linearize.py new file mode 100644 index 00000000..2a5fec13 --- /dev/null +++ b/obol/src/obol/transformers/linearize.py @@ -0,0 +1,287 @@ +"""Hoist nested calls into one-call-per-statement form (ANF like pass)""" + +import libcst as cst + +from obol.transformers.normalize import normalize_function_body, normalize_inline_if + + +class RemoteCallLinearizer(cst.CSTTransformer): + """ + Linearizes remote calls within functions. + """ + + def __init__(self, entities: dict[str, str] | None = None): + self.entities = entities or {} + self.call_counter = 0 + self.current_class: str | None = None + + def visit_ClassDef(self, node: cst.ClassDef) -> bool: + self.current_class = node.name.value + return True + + def leave_ClassDef(self, _original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.ClassDef: + self.current_class = None + return updated_node + + def leave_FunctionDef(self, _original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef: + """Process each function and linearize remote calls.""" + if self.current_class is not None and self.current_class not in self.entities: + return updated_node + + # Normalize inline function bodies + updated_node = normalize_function_body(updated_node) + + self.call_counter = 0 + + linearizer = StatementLinearizer(self.entities) + new_body = updated_node.body.visit(linearizer) + + return updated_node.with_changes(body=new_body) + + +class StatementLinearizer(cst.CSTTransformer): + """ + Linearize method calls within statements. + """ + + def __init__(self, entities: dict[str, str] | None = None): + self.entities = entities or {} + self.counter = 1 + + def leave_SimpleStatementLine( + self, _original_node: cst.SimpleStatementLine, updated_node: cst.SimpleStatementLine + ) -> cst.SimpleStatementLine | cst.FlattenSentinel[cst.SimpleStatementLine]: + """Process each statement and extract method calls.""" + new_statements = [] + + for stmt in updated_node.body: + extractor = CallExtractorAndReplacer(self.entities, self.counter) + new_stmt = stmt.visit(extractor) + + should_collapse = False + last_extracted_var = None + last_extracted_call = None + + if extractor.extracted_calls: + last_extracted_var, last_extracted_call = extractor.extracted_calls[-1] + + if isinstance(new_stmt, cst.Expr) and isinstance(new_stmt.value, cst.Name): + if new_stmt.value.value == last_extracted_var: + should_collapse = True + + elif ( + isinstance(new_stmt, cst.Assign) + and len(new_stmt.targets) == 1 + and isinstance(new_stmt.targets[0].target, cst.Name) + and isinstance(new_stmt.value, cst.Name) + and new_stmt.value.value == last_extracted_var + ): + should_collapse = True + + if should_collapse: + extractor.extracted_calls.pop() + new_stmt = new_stmt.with_changes(value=last_extracted_call) + + self.counter = extractor.counter + + for var_name, call in extractor.extracted_calls: + assignment = cst.SimpleStatementLine( + body=[cst.Assign(targets=[cst.AssignTarget(target=cst.Name(var_name))], value=call)] + ) + new_statements.append(assignment) + + new_statements.append(cst.SimpleStatementLine(body=[new_stmt])) + + return cst.FlattenSentinel(new_statements) + + def leave_If(self, _original_node: cst.If, updated_node: cst.If) -> cst.If | cst.FlattenSentinel[cst.BaseStatement]: + """Handle if statements specially.""" + # Normalize inline if bodies (SimpleStatementSuite -> IndentedBlock) + updated_node = normalize_inline_if(updated_node) + + new_statements = [] + + extractor = CallExtractorAndReplacer(self.entities, self.counter) + new_test = updated_node.test.visit(extractor) + self.counter = extractor.counter + + # If no calls were extracted, return unchanged to avoid + # FlattenSentinel in positions that don't support it (e.g. elif) + if not extractor.extracted_calls: + return updated_node + + for var_name, call in extractor.extracted_calls: + assignment = cst.SimpleStatementLine( + body=[cst.Assign(targets=[cst.AssignTarget(target=cst.Name(var_name))], value=call)] + ) + new_statements.append(assignment) + + new_if = updated_node.with_changes(test=new_test) + new_statements.append(new_if) + + return cst.FlattenSentinel(new_statements) + + def leave_While( + self, _original_node: cst.While, updated_node: cst.While + ) -> cst.While | cst.FlattenSentinel[cst.BaseStatement]: + """Handle while statements specially, extracting from test condition. + + When the condition containsa function call, transform: + while : + + into: + while True: + + if not (): + break + + + This ensures the remote call result is re-fetched each iteration and the + condition is checked against the fresh value, not a stale saved variable. + """ + extractor = CallExtractorAndReplacer(self.entities, self.counter) + new_test = updated_node.test.visit(extractor) + self.counter = extractor.counter + + if not extractor.extracted_calls: + return updated_node + + # Build the extracted call assignments — these go at the top of the body + extracted_assignments = [] + for var_name, call in extractor.extracted_calls: + assignment = cst.SimpleStatementLine( + body=[cst.Assign(targets=[cst.AssignTarget(target=cst.Name(var_name))], value=call)] + ) + extracted_assignments.append(assignment) + + # Build: if not (): break + # Wrap new_test in parens to ensure correct precedence under `not` + parenthesized_test = new_test.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) + not_test = cst.UnaryOperation( + operator=cst.Not(whitespace_after=cst.SimpleWhitespace(" ")), + expression=parenthesized_test, + ) + break_if = cst.If( + test=not_test, + body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Break()])]), + orelse=None, + ) + + # New body: extracted assignments + guard + original body (no end-of-body re-extractions) + new_body_stmts = [*extracted_assignments, break_if, *updated_node.body.body] + new_body = updated_node.body.with_changes(body=new_body_stmts) + + # Replace loop condition with True — the guard above handles termination + return updated_node.with_changes(test=cst.Name("True"), body=new_body) + + def leave_For( + self, _original_node: cst.For, updated_node: cst.For + ) -> cst.For | cst.FlattenSentinel[cst.BaseStatement]: + """Handle for statements specially, extracting from iter condition.""" + new_statements = [] + + extractor = CallExtractorAndReplacer(self.entities, self.counter) + new_iter = updated_node.iter.visit(extractor) + self.counter = extractor.counter + + if not extractor.extracted_calls: + return updated_node + + for var_name, call in extractor.extracted_calls: + assignment = cst.SimpleStatementLine( + body=[cst.Assign(targets=[cst.AssignTarget(target=cst.Name(var_name))], value=call)] + ) + new_statements.append(assignment) + + new_for = updated_node.with_changes(iter=new_iter) + new_statements.append(new_for) + + return cst.FlattenSentinel(new_statements) + + +class CallExtractorAndReplacer(cst.CSTTransformer): + """ + Extract and replace method calls with variables. + """ + + def __init__(self, entities: dict[str, str] | None = None, start_counter=1): + self.entities = entities or {} + self.extracted_calls: list[tuple[str, cst.BaseExpression]] = [] + self.counter = start_counter + self._in_send_async = False + self._in_gather = False + + def visit_Call(self, node: cst.Call) -> bool: + """Set flag when entering send_async()/gather() to prevent extraction of inner calls.""" + if isinstance(node.func, cst.Name) and node.func.value == "send_async": + self._in_send_async = True + elif isinstance(node.func, cst.Name) and node.func.value == "gather": + self._in_gather = True + return True + + def leave_Call(self, _original_node: cst.Call, updated_node: cst.Call) -> cst.BaseExpression: + # Don't extract send_async itself — clear flag and return unchanged + + if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "send_async": + self._in_send_async = False + return updated_node + + # Don't extract gather itself — used for fan-out + # all its inner calls in parallel via a barrier-and-join step. + if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "gather": + self._in_gather = False + return updated_node + + # Don't extract calls inside send_async or gather + if self._in_send_async or self._in_gather: + return updated_node + + # Don't extract self.__key__() + if ( + isinstance(updated_node.func, cst.Attribute) + and isinstance(updated_node.func.value, cst.Name) + and updated_node.func.value.value == "self" + and updated_node.func.attr.value == "__key__" + ): + return updated_node + + is_entity_instantiation = False + if isinstance(updated_node.func, cst.Name) and updated_node.func.value in self.entities: + is_entity_instantiation = True + + if is_entity_instantiation: + receiver_var_name = f"attr_{self.counter}" + self.counter += 1 + self.extracted_calls.append((receiver_var_name, updated_node)) + return cst.Name(receiver_var_name) + + if isinstance(updated_node.func, cst.Attribute): + receiver = updated_node.func.value + new_func = updated_node.func + + # Don't extract simple names or self.attribute as receivers + is_simple_receiver = False + if isinstance(receiver, cst.Name) or ( + isinstance(receiver, cst.Attribute) + and isinstance(receiver.value, cst.Name) + and receiver.value.value == "self" + ): + is_simple_receiver = True + + if not is_simple_receiver: + receiver_var_name = f"attr_{self.counter}" + self.counter += 1 + self.extracted_calls.append((receiver_var_name, receiver)) + + new_func = new_func.with_changes(value=cst.Name(receiver_var_name)) + + new_call = updated_node.with_changes(func=new_func) + + var_name = f"attr_{self.counter}" + self.counter += 1 + + self.extracted_calls.append((var_name, new_call)) + + return cst.Name(var_name) + + return updated_node diff --git a/obol/src/obol/transformers/normalize.py b/obol/src/obol/transformers/normalize.py new file mode 100644 index 00000000..95ce4b3b --- /dev/null +++ b/obol/src/obol/transformers/normalize.py @@ -0,0 +1,29 @@ +"""CST shape normalization helpers used by multiple later passes.""" + +import libcst as cst + + +def normalize_function_body(node: cst.FunctionDef) -> cst.FunctionDef: + """Convert inline functions to normal functions""" + if isinstance(node.body, cst.SimpleStatementSuite): + new_body = cst.IndentedBlock(body=[cst.SimpleStatementLine(body=list(node.body.body))]) + return node.with_changes(body=new_body) + return node + + +def normalize_inline_if(node: cst.If) -> cst.If: + """Normalize inline if statements into regular if statements.""" + # Normalize the if-body + if isinstance(node.body, cst.SimpleStatementSuite): + new_body = cst.IndentedBlock(body=[cst.SimpleStatementLine(body=list(node.body.body))]) + node = node.with_changes(body=new_body) + + # Normalize the else/elif + if node.orelse is not None: + if isinstance(node.orelse, cst.Else) and isinstance(node.orelse.body, cst.SimpleStatementSuite): + new_else_body = cst.IndentedBlock(body=[cst.SimpleStatementLine(body=list(node.orelse.body.body))]) + node = node.with_changes(orelse=node.orelse.with_changes(body=new_else_body)) + elif isinstance(node.orelse, cst.If): + node = node.with_changes(orelse=normalize_inline_if(node.orelse)) + + return node diff --git a/obol/src/obol/transformers/return_handler.py b/obol/src/obol/transformers/return_handler.py new file mode 100644 index 00000000..db71116f --- /dev/null +++ b/obol/src/obol/transformers/return_handler.py @@ -0,0 +1,152 @@ +"""Wrap user `return` statements in `send_reply(...)` and inject `ctx.put(__state__)` before every dispatch/return.""" + +import libcst as cst + + +class ReturnHandlerTransformer(cst.CSTTransformer): + """ + Finds 'return' statements and wraps them with the reply_to stack logic. + Also injects 'ctx.put(state)' immediately before the logic. + """ + + def __init__(self, uses_state: bool): + super().__init__() + + # If we don't use state at all no need to inject ctx.put(__state__) + self.uses_state = uses_state + + # True while inside a gather-join step. Bare `return` inside such steps is basically a yield + # and should not be rewritten to send_reply. + self._in_gather_join = False + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: + if any(p.name.value == "_gather_partial" for p in node.params.params): + self._in_gather_join = True + return True + + def _is_graph_terminal(self, node: cst.CSTNode | None) -> bool: + """ + Recursively checks if a node guarantees an exit (return, raise, or async dispatch). + """ + if node is None: + return False + + if isinstance(node, (cst.Return, cst.Raise, cst.Break, cst.Continue)): + return True + + if isinstance(node, cst.SimpleStatementLine): + # Check for ctx.call_remote_async(...) — after dispatch, function is done + for child in node.body: + if isinstance(child, cst.Expr) and isinstance(child.value, cst.Call): + func = child.value.func + if ( + isinstance(func, cst.Attribute) + and isinstance(func.value, cst.Name) + and func.value.value == "ctx" + and func.attr.value == "call_remote_async" + ): + return True + return any(self._is_graph_terminal(child) for child in node.body) + + if isinstance(node, cst.IndentedBlock): + if not node.body: + return False + return self._is_graph_terminal(node.body[-1]) + + if isinstance(node, cst.If): + if node.orelse is None: + return False + + body_terminal = self._is_graph_terminal(node.body) + + else_terminal = False + if isinstance(node.orelse, cst.Else): + else_terminal = self._is_graph_terminal(node.orelse.body) + elif isinstance(node.orelse, cst.If): # elif chain + else_terminal = self._is_graph_terminal(node.orelse) + + return body_terminal and else_terminal + + return False + + def _is_call_remote_async(self, node: cst.CSTNode) -> bool: + """Check if a statement is a ctx.call_remote_async(...) call.""" + if isinstance(node, cst.SimpleStatementLine): + for el in node.body: + if isinstance(el, cst.Expr) and isinstance(el.value, cst.Call): + func = el.value.func + if ( + isinstance(func, cst.Attribute) + and isinstance(func.value, cst.Name) + and func.value.value == "ctx" + and func.attr.value == "call_remote_async" + ): + return True + return False + + def _has_reply_to_param(self, node: cst.FunctionDef) -> bool: + """Check if a function has a reply_to parameter.""" + return any(param.name.value == "reply_to" for param in node.params.params) + + def leave_SimpleStatementLine(self, _original_node, updated_node): + # Handle ctx.call_remote_async: prepend ctx.put(__state__) if uses_state + if self._is_call_remote_async(updated_node) and self.uses_state: + put_state = cst.parse_statement("ctx.put(__state__)") + return cst.FlattenSentinel([put_state, updated_node]) + + return_node = None + for node in updated_node.body: + if isinstance(node, cst.Return): + return_node = node + break + + if not return_node: + return updated_node + + # This means we have not yet received all the replies so we should not send a reply yet. + if self._in_gather_join and return_node.value is None: + return updated_node + + ret_val = return_node.value if return_node.value else cst.Name("None") + + # Ensure implicit tuples (return a, b) get parenthesized so they + # become a single argument: send_reply(ctx, reply_to, (a, b)) + if isinstance(ret_val, cst.Tuple) and not ret_val.lpar: + ret_val = ret_val.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) + + # Generate: return send_reply(ctx, reply_to, result) + send_reply_call = cst.Call( + func=cst.Name("send_reply"), + args=[ + cst.Arg(value=cst.Name("ctx")), + cst.Arg(value=cst.Name("reply_to")), + cst.Arg(value=ret_val), + ], + ) + + res_stmt = cst.SimpleStatementLine(body=[cst.Return(value=send_reply_call)]) + + put_state = cst.parse_statement("ctx.put(__state__)") + + return cst.FlattenSentinel([put_state, res_stmt]) if self.uses_state else res_stmt + + def leave_FunctionDef(self, _original_node, updated_node): + body_stmts = updated_node.body.body + last_stmt = body_stmts[-1] if body_stmts else None + + if not self._is_graph_terminal(last_stmt): + new_body = list(updated_node.body.body) + # Add send_reply for functions with reply_to so the reply chain isn't lost + if self._has_reply_to_param(updated_node): + if self.uses_state: + new_body.append(cst.parse_statement("ctx.put(__state__)")) + if not self._in_gather_join: + new_body.append(cst.parse_statement("return send_reply(ctx, reply_to, None)")) + elif self.uses_state: + # No reply_to but state used update state + new_body.append(cst.parse_statement("ctx.put(__state__)")) + + if new_body != list(updated_node.body.body): + return updated_node.with_changes(body=updated_node.body.with_changes(body=new_body)) + + return updated_node diff --git a/obol/src/obol/transformers/short_circuit.py b/obol/src/obol/transformers/short_circuit.py new file mode 100644 index 00000000..4b110bed --- /dev/null +++ b/obol/src/obol/transformers/short_circuit.py @@ -0,0 +1,239 @@ +"""Rewrite boolean expressions containing remote calls into explicit ifs to preserve short circuit semantics.""" + +import libcst as cst + + +def _has_extractable_call(node: cst.CSTNode, entities: dict[str, str], in_send_async: bool = False) -> bool: + """Does this subtree contain a call that the extractor would hoist? + + Mirrors CallExtractorAndReplacer's extraction rules: entity constructors and + any attribute-style call count; send_async, gather, and self.__key__() do not. + """ + if isinstance(node, cst.Call): + if isinstance(node.func, cst.Name) and node.func.value in ("send_async", "gather"): + return False + if in_send_async: + return False + if ( + isinstance(node.func, cst.Attribute) + and isinstance(node.func.value, cst.Name) + and node.func.value.value == "self" + and node.func.attr.value == "__key__" + ): + return False + if isinstance(node.func, cst.Name) and node.func.value in entities: + return True + if isinstance(node.func, cst.Attribute): + return True + + return any(_has_extractable_call(child, entities, in_send_async) for child in node.children) + + +class ShortCircuitRewriter(cst.CSTTransformer): + """ + Rewrite short-circuiting boolean operations that would be hoisted by linearizer + into if-statements, so that we preserve short-circuit semantics. + + Example: + + if a.get_stock() > 0 and a.get_price() < 100: + body + + becomes: + + _sc_1 = a.get_stock() > 0 + if _sc_1: + _sc_1 = a.get_price() < 100 + if _sc_1: + body + + These will then be hoisted by the linearzer, but the short-circuit semantics are preserved by the nested ifs. + """ + + def __init__(self, entities: dict[str, str] | None = None): + super().__init__() + self.entities = entities or {} + self.current_class: str | None = None + self._counter_stack: list[int] = [0] + + def visit_ClassDef(self, node: cst.ClassDef) -> bool: + self.current_class = node.name.value + return True + + def leave_ClassDef(self, _original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.ClassDef: + self.current_class = None + return updated_node + + def visit_FunctionDef(self, _node: cst.FunctionDef) -> bool: + self._counter_stack.append(0) + return True + + def leave_FunctionDef(self, _original_node, updated_node): + self._counter_stack.pop() + return updated_node + + def _next_var(self) -> str: + self._counter_stack[-1] += 1 + return f"_sc_{self._counter_stack[-1]}" + + def _rewrite_boolop(self, expr: cst.BaseExpression) -> tuple[list[cst.BaseStatement], cst.BaseExpression]: + """Rewrite a BooleanOperation containing extractable calls. + + Returns (hoisted_statements, new_expression). If expr is not a + BooleanOperation or doesn't contain any extractable call, returns + ([], expr) unchanged. + """ + if not isinstance(expr, cst.BooleanOperation): + return [], expr + if not _has_extractable_call(expr, self.entities): + return [], expr + + left_hoist, left_expr = self._rewrite_boolop(expr.left) + right_hoist, right_expr = self._rewrite_boolop(expr.right) + + sc_var = self._next_var() + + init_stmt = cst.SimpleStatementLine( + body=[ + cst.Assign( + targets=[cst.AssignTarget(target=cst.Name(sc_var))], + value=left_expr, + ) + ] + ) + + # `a and b` → compute b only if _sc is truthy + # `a or b` → compute b only if _sc is falsy + if isinstance(expr.operator, cst.Or): + inner_test: cst.BaseExpression = cst.UnaryOperation( + operator=cst.Not(whitespace_after=cst.SimpleWhitespace(" ")), + expression=cst.Name(sc_var), + ) + else: + inner_test = cst.Name(sc_var) + + inner_body_stmts = [ + *right_hoist, + cst.SimpleStatementLine( + body=[ + cst.Assign( + targets=[cst.AssignTarget(target=cst.Name(sc_var))], + value=right_expr, + ) + ] + ), + ] + + if_block = cst.If( + test=inner_test, + body=cst.IndentedBlock(body=inner_body_stmts), + orelse=None, + ) + + return [*left_hoist, init_stmt, if_block], cst.Name(sc_var) + + def _rewrite_in_simple_stmt(self, stmt: cst.SimpleStatementLine) -> list[cst.BaseStatement]: + """Rewrite top-level BoolOps in each small-statement's primary expression.""" + all_hoisted: list[cst.BaseStatement] = [] + new_body: list[cst.BaseSmallStatement] = [] + changed = False + + for small in stmt.body: + if ( + isinstance(small, cst.Assign) + or (isinstance(small, cst.AnnAssign) and small.value is not None) + or (isinstance(small, cst.Return) and small.value is not None) + or isinstance(small, cst.Expr) + ): + hoisted, new_value = self._rewrite_boolop(small.value) + if hoisted: + all_hoisted.extend(hoisted) + new_body.append(small.with_changes(value=new_value)) + changed = True + else: + new_body.append(small) + else: + new_body.append(small) + + if changed: + return [*all_hoisted, stmt.with_changes(body=new_body)] + return [stmt] + + def _rewrite_if_chain(self, if_stmt: cst.If) -> list[cst.BaseStatement]: + """Rewrite BoolOps in an if/elif chain's tests. + + Elif chains are flattened into nested if/else so hoisted statements can + live inside the preceding else branch. + """ + hoisted, new_test = self._rewrite_boolop(if_stmt.test) + + # Recurse into the else/elif. + new_orelse: cst.Else | cst.If | None = if_stmt.orelse + if isinstance(if_stmt.orelse, cst.If): + # Elif: rewrite it as a list of statements. + inner = self._rewrite_if_chain(if_stmt.orelse) + if len(inner) == 1 and isinstance(inner[0], cst.If): + new_orelse = inner[0] + else: + new_orelse = cst.Else(body=cst.IndentedBlock(body=inner)) + # Else blocks and nested statements inside them are already handled by + # leave_IndentedBlock recursively + + new_if = if_stmt.with_changes(test=new_test, orelse=new_orelse) + + if hoisted: + return [*hoisted, new_if] + return [new_if] + + def _rewrite_while(self, while_stmt: cst.While) -> list[cst.BaseStatement]: + """Rewrite BoolOps in a while-loop test. + + Since the test must re-evaluate each iteration, the hoisted statements + are pushed into the body and the loop becomes `while True` with an + explicit break guard. The later RemoteCallLinearizer.leave_While + normalization is happy with this shape. + """ + hoisted, new_test = self._rewrite_boolop(while_stmt.test) + if not hoisted: + return [while_stmt] + + parenthesized = ( + new_test + if isinstance(new_test, cst.Name) + else new_test.with_changes(lpar=[cst.LeftParen()], rpar=[cst.RightParen()]) + ) + break_if = cst.If( + test=cst.UnaryOperation( + operator=cst.Not(whitespace_after=cst.SimpleWhitespace(" ")), + expression=parenthesized, + ), + body=cst.IndentedBlock(body=[cst.SimpleStatementLine(body=[cst.Break()])]), + orelse=None, + ) + new_body_stmts = [*hoisted, break_if, *list(while_stmt.body.body)] + return [ + while_stmt.with_changes( + test=cst.Name("True"), + body=while_stmt.body.with_changes(body=new_body_stmts), + ) + ] + + def _rewrite_statement(self, stmt: cst.BaseStatement) -> list[cst.BaseStatement]: + if isinstance(stmt, cst.If): + return self._rewrite_if_chain(stmt) + if isinstance(stmt, cst.While): + return self._rewrite_while(stmt) + if isinstance(stmt, cst.SimpleStatementLine): + return self._rewrite_in_simple_stmt(stmt) + return [stmt] + + def leave_IndentedBlock( + self, _original_node: cst.IndentedBlock, updated_node: cst.IndentedBlock + ) -> cst.IndentedBlock: + if self.current_class is not None and self.current_class not in self.entities: + return updated_node + + new_body: list[cst.BaseStatement] = [] + for stmt in updated_node.body: + new_body.extend(self._rewrite_statement(stmt)) + return updated_node.with_changes(body=new_body) diff --git a/obol/src/obol/transformers/state_access.py b/obol/src/obol/transformers/state_access.py new file mode 100644 index 00000000..52ae56e8 --- /dev/null +++ b/obol/src/obol/transformers/state_access.py @@ -0,0 +1,143 @@ +"""Rewrite `self.attr`, `self`, `self.__key__()`, `get_entity_by_key`, and `exists()`.""" + +import libcst as cst +import libcst.matchers as m + + +class StateAccessTransformer(cst.CSTTransformer): + """ + Transforms: + 1. self.attribute -> __state__['attribute'] + 2. self -> ctx.key + 3. self.__key__() -> ctx.key + 4. get_entity_by_key(Entity, key) -> key + 5. exists(self) -> bool(__state__) + """ + + def __init__(self, metadata=None, entity_keys=None, entity_init_params=None): + super().__init__() + self.metadata = metadata or {} + self.entity_keys = entity_keys or {} + self.entity_init_params = entity_init_params or {} + + def _get_node_type(self, node): + """Helper to get the type name from mypy metadata or CST node type (for literals).""" + mypy_type = self.metadata.get(node) + if mypy_type: + # Simple extraction for the print statement + fullname = mypy_type.fullname + type_name = fullname.rsplit(".", 1)[-1] + + # Handle union types like "int | None" + if "|" in type_name: + type_name = type_name.split("|")[0].strip() + return type_name + + # Fallback for literals + if isinstance(node, cst.SimpleString): + return "str" + if isinstance(node, cst.Integer): + return "int" + if isinstance(node, cst.Float): + return "float" + if isinstance(node, cst.Name) and node.value in ("True", "False"): + return "bool" + + return "None" + + def leave_Call(self, original_node, updated_node): + # Handles self.__key__() -> ctx.key + if m.matches(original_node, m.Call(func=m.Attribute(value=m.Name("self"), attr=m.Name("__key__")))): + return cst.Attribute(value=cst.Name("ctx"), attr=cst.Name("key")) + + # Handles exists(self) -> bool(__state__). Currently only supports exists(self), maybe could be extended? + if m.matches(original_node, m.Call(func=m.Name("exists"))): + if len(original_node.args) != 1 or not m.matches(original_node.args[0].value, m.Name("self")): + msg = "exists() currently only supports a single argument 'self'" + raise ValueError(msg) + return cst.Call(func=cst.Name("bool"), args=[cst.Arg(value=cst.Name("__state__"))]) + + # Handles get_entity_by_key(Entity, key) -> key (or concatenated string for tuples) + if m.matches(original_node, m.Call(func=m.Name("get_entity_by_key"))) and len(updated_node.args) >= 2: + entity_node = updated_node.args[0].value + key = updated_node.args[1].value + + if isinstance(entity_node, cst.Name): + entity_name = entity_node.value + key_attrs = self.entity_keys.get(entity_name, []) + init_params = self.entity_init_params.get(entity_name, {}) + expected_types = [init_params.get(a) for a in key_attrs] + + # 1. Structure validation (only possible for literal tuple keys) + if len(key_attrs) > 1 and isinstance(key, cst.Tuple) and len(key.elements) != len(key_attrs): + msg = ( + f"get_entity_by_key for {entity_name} expects " + f"{len(key_attrs)} elements, but got {len(key.elements)}" + ) + raise TypeError(msg) + + # 2. Collect types for comparison and reporting + if isinstance(key, cst.Tuple): + actual_types = [] + original_tuple = original_node.args[1].value + for i in range(len(key.elements)): + original_element = original_tuple.elements[i].value + actual_types.append(self._get_node_type(original_element)) + + # Check for mismatches (skip if actual type could not be resolved) + for _i, (actual, expected) in enumerate(zip(actual_types, expected_types, strict=True)): + if actual not in (None, "None") and actual != expected: + expected_str = f"({', '.join(expected_types)})" + actual_str = f"({', '.join(actual_types)})" + msg = ( + f"Type mismatch for retrieving '{entity_name}' by key: " + f"expected {expected_str}, got {actual_str}" + ) + raise TypeError(msg) + + # Build: ctx.key = f"{key[0]}:{key[1]}" + parts = [] + + for i, el in enumerate(key.elements): + if i > 0: + parts.append(cst.FormattedStringText(":")) + + parts.append(cst.FormattedStringExpression(expression=el.value)) + + return cst.FormattedString(parts=parts) + original_key = original_node.args[1].value + actual_type = self._get_node_type(original_key) + expected_type = expected_types[0] if expected_types else None + if actual_type not in (None, "None") and (expected_type != "None") and actual_type != expected_type: + msg = ( + f"Type mismatch for retrieving '{entity_name}' by key: " + f"expected {expected_type}, got {actual_type}" + ) + raise TypeError(msg) + + return updated_node.args[1].value + + return updated_node + + def leave_AnnAssign(self, _original_node, updated_node): + # Python does not support __state__['x']: int = 5, so we need to remove the annotation + # for things that are transformed to state access (e.g. self.x: int = 5 -> __state__['x'] = 5). + if isinstance(updated_node.target, cst.Subscript): + value = updated_node.value if updated_node.value is not None else cst.Name("None") + return cst.Assign(targets=[cst.AssignTarget(target=updated_node.target)], value=value) + return updated_node + + def leave_Attribute(self, original_node, updated_node): + # Handles self.attribute -> __state__['attribute'] + if m.matches(original_node, m.Attribute(value=m.Name("self"))): + return cst.Subscript( + value=cst.Name("__state__"), + slice=[cst.SubscriptElement(slice=cst.Index(value=cst.SimpleString(f"'{original_node.attr.value}'")))], + ) + return updated_node + + def leave_Name(self, original_node, updated_node): + # Handles standalone 'self' -> 'ctx.key' + if m.matches(original_node, m.Name("self")): + return cst.Attribute(value=cst.Name("ctx"), attr=cst.Name("key")) + return updated_node diff --git a/obol/src/obol/visitor.py b/obol/src/obol/visitor.py new file mode 100644 index 00000000..58a8b630 --- /dev/null +++ b/obol/src/obol/visitor.py @@ -0,0 +1,66 @@ +""" +Visitor classes for the Styx transpiler. +""" + +import libcst as cst +import libcst.matchers as m + + +class EntityDiscoveryVisitor(cst.CSTVisitor): + """ + Discovers entity classes marked with @entity decorator. + """ + + def __init__(self): + super().__init__() + self.entities = {} + self.entity_keys = {} # Stores list of attribute names: {"Item": ["item_name"], "Stock": ["w_id", "i_id"]} + self.entity_key_types = {} # e.g. {"Item": "str"} + self.entity_init_params = {} # e.g. {"Item": {"item_name": "str", "price": "int"}} + + def visit_ClassDef(self, node: cst.ClassDef): + for decorator in node.decorators: + if m.matches(decorator, m.Decorator(decorator=m.Name("entity"))): + class_name = node.name.value + self.entities[class_name] = class_name.lower() + + for item in node.body.body: + # Find the __key__ method to identify the key field(s) + if isinstance(item, cst.FunctionDef) and item.name.value == "__key__": + # Record the return type of __key__ if explicitly annotated + if item.returns and isinstance(item.returns.annotation, cst.Name): + self.entity_key_types[class_name] = item.returns.annotation.value + + for stmt in item.body.body: + if m.matches(stmt, m.SimpleStatementLine(body=[m.Return()])): + ret_val = stmt.body[0].value + if ret_val is None: + continue + + # Case 1: return self.attr + if m.matches(ret_val, m.Attribute(value=m.Name("self"), attr=m.Name())): + self.entity_keys[class_name] = [ret_val.attr.value] + + # Case 2: return (self.a, self.b, ...) + elif isinstance(ret_val, cst.Tuple): + attrs = [] + is_valid = True + for element in ret_val.elements: + if m.matches(element.value, m.Attribute(value=m.Name("self"), attr=m.Name())): + attrs.append(element.value.attr.value) + else: + is_valid = False + break + if is_valid and attrs: + self.entity_keys[class_name] = attrs + + # Find __init__ to record parameter names and types (excluding 'self') + if isinstance(item, cst.FunctionDef) and item.name.value == "__init__": + params = {} + for p in item.params.params: + if p.name.value != "self": + type_str = None + if p.annotation and isinstance(p.annotation.annotation, cst.Name): + type_str = p.annotation.annotation.value + params[p.name.value] = type_str + self.entity_init_params[class_name] = params diff --git a/obol/tests/__init__.py b/obol/tests/__init__.py new file mode 100644 index 00000000..41b8624c --- /dev/null +++ b/obol/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Testing module for obol +""" diff --git a/obol/tests/test_comprehension_expander.py b/obol/tests/test_comprehension_expander.py new file mode 100644 index 00000000..6a763a67 --- /dev/null +++ b/obol/tests/test_comprehension_expander.py @@ -0,0 +1,83 @@ +"""Tests for the ComprehensionExpander transformer.""" + +import libcst as cst + +from obol.comprehension_expander import ComprehensionExpander + + +def _expand(code: str) -> str: + """Parse, expand comprehensions, return resulting code.""" + tree = cst.parse_module(code) + expander = ComprehensionExpander() + new_tree = tree.visit(expander) + return new_tree.code + + +def test_list_comprehension(): + code = "result = [x * 2 for x in items]\n" + output = _expand(code) + assert "for x in items:" in output + assert ".append(x * 2)" in output + assert "_comp_result_1" in output + assert "[x * 2 for x in items]" not in output + + +def test_dict_comprehension(): + code = "result = {k: v for k, v in pairs}\n" + output = _expand(code) + assert "for k, v in pairs:" in output + assert "_comp_result_1" in output + assert "{k: v for k, v in pairs}" not in output + + +def test_set_comprehension(): + code = "result = {x for x in items}\n" + output = _expand(code) + assert "for x in items:" in output + assert ".add(x)" in output + assert "_comp_result_1" in output + assert "{x for x in items}" not in output + + +def test_list_comprehension_with_filter(): + code = "result = [x for x in items if x > 0]\n" + output = _expand(code) + assert "for x in items:" in output + assert "if x > 0:" in output + assert ".append(x)" in output + + +def test_nested_comprehension(): + code = "result = [x + y for x in xs for y in ys]\n" + output = _expand(code) + assert "for x in xs:" in output + assert "for y in ys:" in output + assert ".append(x + y)" in output + + +def test_no_comprehension_unchanged(): + code = "x = 42\n" + output = _expand(code) + assert output.strip() == "x = 42" + + +def test_multiple_comprehensions_in_function(): + code = """\ +def f(): + a = [x for x in items] + b = {x: x for x in items} + c = {x for x in items} +""" + output = _expand(code) + assert "_comp_result_1" in output + assert "_comp_result_2" in output + assert "_comp_result_3" in output + + +def test_comprehension_with_method_call(): + """Matches the user_item.py example: item.get_stock() for item in items.""" + code = "stock_values = [item.get_stock() for item in items]\n" + output = _expand(code) + assert "for item in items:" in output + assert ".append(item.get_stock())" in output + assert "_comp_result_1" in output diff --git a/obol/tests/test_differential.py b/obol/tests/test_differential.py new file mode 100644 index 00000000..2f0c8eab --- /dev/null +++ b/obol/tests/test_differential.py @@ -0,0 +1,467 @@ +""" +Randomized differential test: local entities vs compiled Styx operators. + +A seeded generator produces random sequences of entity operations. Each +operation runs on the local Python classes (the reference) and is replayed +on Styx, comparing return values and, after every op, the observable state +of all entities. Known semantic differences are normalized away: + - entity references: objects locally, key strings on Styx + - domain exceptions: raised locally, returned as error strings by Styx +One extra detail makes the local reference sound: Styx rolls back the whole +transaction on an exception, plain Python doesn't (e.g. bulk_purchase +mutates earlier items before raising for a later one). So we snapshot the +local world before each op and restore it when a domain exception fires. + +Requires a running Styx deployment + +Run: uv run pytest tests/test_differential.py -v (with Styx running) +Reproduce a failure: set SEED to the seed printed in the failure message. +Tune with env vars: SEED, SEQUENCES, OPS, OBOL_EXAMPLE. +""" + +import copy +import importlib.util +import os +import pathlib +import random +import socket +import sys +import unittest +import uuid +from collections import Counter +from time import sleep + +from styx.client.sync_client import SyncStyxClient +from styx.common.local_state_backends import LocalStateBackend +from styx.common.stateflow_graph import StateflowGraph + +ROOT = pathlib.Path(__file__).resolve().parent.parent +EXAMPLE = os.environ.get("OBOL_EXAMPLE", "user_item") + + +def _load_module(path: pathlib.Path, name: str): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +m = _load_module(ROOT / "examples" / "original" / f"{EXAMPLE}.py", "obol_reference") + +m.gather = lambda *args: args + +compiled = _load_module(ROOT / "examples" / "compiled" / f"{EXAMPLE}.py", "obol_compiled") +item_operator = compiled.item_operator +user_operator = compiled.user_operator +coupon_operator = compiled.coupon_operator + +SEED = int(os.environ.get("SEED", "42")) +N_SEQUENCES = int(os.environ.get("SEQUENCES", "10")) +OPS_PER_SEQ = int(os.environ.get("OPS", "300")) + +# Connection targets (override via env to match your deployment). +STYX_HOST = os.environ.get("STYX_HOST", "localhost") +STYX_PORT = int(os.environ.get("STYX_PORT", "8886")) +KAFKA_URL = os.environ.get("KAFKA_URL", "localhost:9092") +styx = None + + +def _reachable(host, port, timeout=2.0): + try: + with socket.create_connection((host, int(port)), timeout=timeout): + return True + except OSError: + return False + + +def setUpModule(): + global styx + kafka_host, _, kafka_port = KAFKA_URL.partition(":") + if not _reachable(STYX_HOST, STYX_PORT) or not _reachable(kafka_host, kafka_port or 9092): + raise unittest.SkipTest( + f"Styx is not running: this differential test replays operations on a live Styx " + f"deployment and needs the coordinator at {STYX_HOST}:{STYX_PORT} and Kafka at " + f"{KAFKA_URL}. Start Styx first (e.g. `docker compose up` from the styx repo root), " + f"then re-run." + ) + try: + print(f"[setup] connecting to Styx at {STYX_HOST}:{STYX_PORT}, kafka={KAFKA_URL}", flush=True) + styx = SyncStyxClient(STYX_HOST, STYX_PORT, kafka_url=KAFKA_URL) + g = StateflowGraph("wdm-difftest", operator_state_backend=LocalStateBackend.DICT) + item_operator.set_n_partitions(1) + user_operator.set_n_partitions(1) + coupon_operator.set_n_partitions(1) + g.add_operators(item_operator, user_operator, coupon_operator) + print("[setup] submitting dataflow graph...", flush=True) + styx.submit_dataflow(g) + print("[setup] graph submitted; waiting for workers to register...", flush=True) + sleep(10) + print("[setup] opening client (starting Kafka consumers)...", flush=True) + styx.open(consume=True) + print("[setup] ready.", flush=True) + except Exception as e: + raise unittest.SkipTest(f"Could not connect to a running Styx deployment: {e}") from e + + +def tearDownModule(): + if styx: + styx.close() + + +# -------------------------------------------------------------------------- +# World: paired local objects and Styx entities, created from one seed +# -------------------------------------------------------------------------- + + +class Ref: + """Placeholder for an entity in generated args: resolved to the local + object on the local side and to the key string on the Styx side.""" + + def __init__(self, kind, idx): + self.kind, self.idx = kind, idx + + def __repr__(self): + return f"<{self.kind}{self.idx}>" + + +class World: + def __init__(self, rng): + self.rng = rng + self.users, self.items, self.coupons = [], [], [] # local objects + self.user_keys, self.item_keys, self.coupon_keys = [], [], [] # styx keys + for _ in range(rng.randint(2, 3)): + self._new_user(rng.randint(0, 300)) + for _ in range(rng.randint(3, 5)): + self._new_item(rng.randint(1, 100), rng.randint(0, 12)) + for _ in range(rng.randint(1, 3)): + self._new_coupon(rng.randint(0, 60)) + + def _send(self, op, key, fn, params=()): + return styx.send_event(operator=op, key=key, function=fn, params=params).get().response + + def _new_user(self, balance): + key = str(uuid.uuid4()) + u = m.User(key) + self._send(user_operator, key, "insert", (key,)) + if balance: + u.add_balance(balance) + self._send(user_operator, key, "add_balance", (balance,)) + self.users.append(u) + self.user_keys.append(key) + + def _new_item(self, price, stock): + key = str(uuid.uuid4()) + it = m.Item(key, price) + self._send(item_operator, key, "insert", (key, price)) + if stock: + it.update_stock(stock) + self._send(item_operator, key, "update_stock", (stock,)) + self.items.append(it) + self.item_keys.append(key) + + def _new_coupon(self, discount): + key = str(uuid.uuid4()) + c = m.Coupon(key, discount) + self._send(coupon_operator, key, "insert", (key, discount)) + self.coupons.append(c) + self.coupon_keys.append(key) + + def _lists(self, kind): + return { + "user": (self.users, self.user_keys), + "item": (self.items, self.item_keys), + "coupon": (self.coupons, self.coupon_keys), + }[kind] + + # token maps so both sides normalize to the same canonical names + def token(self, key): + if key in self.user_keys: + return f"" + if key in self.item_keys: + return f"" + if key in self.coupon_keys: + return f"" + return key + + def resolve_local(self, a): + if isinstance(a, Ref): + return self._lists(a.kind)[0][a.idx] + return [self.resolve_local(x) for x in a] if isinstance(a, list) else a + + def resolve_styx(self, a): + if isinstance(a, Ref): + return self._lists(a.kind)[1][a.idx] + return [self.resolve_styx(x) for x in a] if isinstance(a, list) else a + + def normalize(self, v): + """Canonicalize for comparison: entities/keys -> tokens, tuples -> lists.""" + if isinstance(v, (m.User, m.Item, m.Coupon)): + return self.token(v.__key__()) + if isinstance(v, str): + return self.token(v) + if isinstance(v, dict): + return {self.normalize(k): self.normalize(x) for k, x in v.items()} + if isinstance(v, (list, tuple)): + return [self.normalize(x) for x in v] + return v + + def local_state(self): + s = {} + for i, u in enumerate(self.users): + s[f""] = (u.get_balance(), [self.token(it.__key__()) for it in u.get_items()]) + for i, it in enumerate(self.items): + s[f""] = (it.get_price(), it.get_stock()) + for i, c in enumerate(self.coupons): + s[f""] = c.get_discount() + return s + + def styx_state(self): + s = {} + for i, key in enumerate(self.user_keys): + items = self._send(user_operator, key, "get_items") or [] + s[f""] = (self._send(user_operator, key, "get_balance"), [self.token(k) for k in items]) + for i, key in enumerate(self.item_keys): + s[f""] = (self._send(item_operator, key, "get_price"), self._send(item_operator, key, "get_stock")) + for i, key in enumerate(self.coupon_keys): + s[f""] = self._send(coupon_operator, key, "get_discount") + return s + + +# -------------------------------------------------------------------------- +# Operation generators: (kind, idx, method, args) with args biased toward +# boundaries. Each may inspect the live local world; None if not applicable. +# -------------------------------------------------------------------------- + + +def _items(rng, w, nmax, nmin=0): + return [Ref("item", rng.randrange(len(w.items))) for _ in range(rng.randint(nmin, nmax))] + + +def _coupons(rng, w, nmax, nmin=0): + return [Ref("coupon", rng.randrange(len(w.coupons))) for _ in range(rng.randint(nmin, nmax))] + + +def g_update_stock(rng, w): + i = rng.randrange(len(w.items)) + stock = w.items[i].get_stock() + amt = rng.choice([rng.randint(0, 10), -stock, -(stock + 1)]) + return "item", i, "update_stock", (amt,) + + +def g_add_balance(rng, w): + return "user", rng.randrange(len(w.users)), "add_balance", (rng.randint(0, 150),) + + +def g_buy_item(rng, w): + u, i = rng.randrange(len(w.users)), rng.randrange(len(w.items)) + price = w.items[i].get_price() + opts = [rng.randint(0, 5), w.items[i].get_stock(), w.items[i].get_stock() + 1] + if price > 0: + opts.append(min(w.users[u].get_balance() // price, 30)) # exactly affordable + return "user", u, "buy_item", (max(rng.choice(opts), 0), Ref("item", i)) + + +def g_drain_stock(rng, w): + small = [i for i, it in enumerate(w.items) if it.get_stock() <= 10] + if not small: + return None + return "user", rng.randrange(len(w.users)), "drain_stock", (Ref("item", rng.choice(small)),) + + +def g_bulk(rng, w): + items = _items(rng, w, 3, nmin=1) + qty = [rng.choice([rng.randint(0, 8), rng.randint(11, 14), rng.randint(51, 55)]) for _ in items] + return "user", rng.randrange(len(w.users)), "bulk_purchase_with_tiers", (items, qty) + + +def g_process_cart(rng, w): + small = [i for i, it in enumerate(w.items) if it.get_stock() <= 8] + cart = [Ref("item", rng.choice(small)) for _ in range(rng.randint(0, 4))] if small else [] + return "user", rng.randrange(len(w.users)), "process_cart_with_limits", (cart, rng.randint(0, 150)) + + +def g_transfer(rng, w): + a, b = rng.randrange(len(w.users)), rng.randrange(len(w.users)) + bal = w.users[a].get_balance() + amt = rng.choice([rng.randint(0, bal + 20), bal, bal + 1]) + return "user", a, "transfer_balance", (Ref("user", b), amt) + + +def g_multi_restock(rng, w): + items = _items(rng, w, 4) + n = max(len(items) + rng.randint(-1, 1), 0) # zip truncation edge + return "user", rng.randrange(len(w.users)), "multi_restock", (items, [rng.randint(0, 10) for _ in range(n)]) + + +def g_user_noargs(method): + return lambda rng, w: ("user", rng.randrange(len(w.users)), method, ()) + + +def g_item_list(method, nmax): + return lambda rng, w: ("user", rng.randrange(len(w.users)), method, (_items(rng, w, nmax),)) + + +def g_item_arg(method): + return lambda rng, w: ("user", rng.randrange(len(w.users)), method, (Ref("item", rng.randrange(len(w.items))),)) + + +def g_discounted_sum(rng, w): + return "user", rng.randrange(len(w.users)), "discounted_sum", (_items(rng, w, 5), rng.randint(0, 100)) + + +def g_demo2(rng, w): + arg = None if rng.random() < 0.3 else Ref("item", rng.randrange(len(w.items))) + return "user", rng.randrange(len(w.users)), "demo2", (arg,) + + +# -- coupon / gather operations --------------------------------------------- + + +def g_get_discounted_price(rng, w): + # discount may exceed price (clamped by max(...,0)) — both cases occur + return ( + "user", + rng.randrange(len(w.users)), + "get_discounted_price", + (Ref("item", rng.randrange(len(w.items))), Ref("coupon", rng.randrange(len(w.coupons)))), + ) + + +def g_buy_with_coupon(rng, w): + coupon = None if rng.random() < 0.3 else Ref("coupon", rng.randrange(len(w.coupons))) + return "user", rng.randrange(len(w.users)), "buy_with_coupon", (Ref("item", rng.randrange(len(w.items))), coupon) + + +def g_gather_in_loop(rng, w): + # zip truncation edge again, this time around a gather inside the loop + return "user", rng.randrange(len(w.users)), "gather_in_loop", (_items(rng, w, 3), _coupons(rng, w, 3)) + + +def g_price_check(rng, w): + # 3-way gather across two entity types: stresses barrier tag ordering + return ( + "user", + rng.randrange(len(w.users)), + "price_check", + ( + Ref("item", rng.randrange(len(w.items))), + Ref("item", rng.randrange(len(w.items))), + Ref("coupon", rng.randrange(len(w.coupons))), + ), + ) + + +GENERATORS = [ + (g_update_stock, 4), + (g_add_balance, 3), + (g_buy_item, 5), + (g_drain_stock, 3), + (g_bulk, 3), + (g_process_cart, 3), + (g_transfer, 3), + (g_multi_restock, 3), + (g_discounted_sum, 2), + (g_demo2, 1), + (g_get_discounted_price, 3), + (g_buy_with_coupon, 4), + (g_gather_in_loop, 3), + (g_price_check, 3), + (g_user_noargs("get_balance"), 1), + (g_user_noargs("get_items"), 1), + (g_user_noargs("inventory_value"), 2), + (g_user_noargs("my_item_prices"), 2), + (g_user_noargs("most_valuable_item_price"), 1), + (g_user_noargs("inventory_value_gather"), 2), + (g_item_list("simple_loop", 5), 2), + (g_item_list("recursion_test", 5), 2), + (g_item_list("comprehensions", 4), 2), + (g_item_list("can_afford_cart", 4), 1), + (g_item_list("group_items_by_price_bucket", 5), 2), + (g_item_arg("ret_tuple"), 1), + (g_item_arg("ret_dict"), 1), + (g_item_arg("is_in_stock"), 1), + (g_item_arg("temp_func"), 1), +] +# Excluded by design: fire_and_forget/demo (send_async is a compile-time +# marker; a local stub cannot suppress evaluation of its argument, so the +# reference cannot execute it faithfully) and type_test (compiler smoke test). + +DOMAIN_EXC = (m.NotEnoughBalance, m.OutOfStock) + + +# -------------------------------------------------------------------------- +# The test +# -------------------------------------------------------------------------- + + +class TestDifferential(unittest.TestCase): + def _run_op(self, w, kind, idx, method, args): + """Run one op on both sides; return mismatch string or None.""" + # local, with transactional-rollback semantics + snapshot = copy.deepcopy((w.users, w.items, w.coupons)) + target = w._lists(kind)[0][idx] + try: + local = ("ok", getattr(target, method)(*[w.resolve_local(a) for a in args])) + except DOMAIN_EXC as e: + w.users, w.items, w.coupons = snapshot + local = ("exc", str(e)) + except Exception: + w.users, w.items, w.coupons = snapshot + return "SKIP" # generator produced an ill-formed op; discard + + # styx + op = {"user": user_operator, "item": item_operator, "coupon": coupon_operator}[kind] + key = w._lists(kind)[1][idx] + resp = ( + styx.send_event(operator=op, key=key, function=method, params=tuple(w.resolve_styx(a) for a in args)) + .get() + .response + ) + + # compare results (Styx returns the exception message string verbatim) + if local[0] == "exc": + if not isinstance(resp, str) or resp != local[1]: + return f"error mismatch: local={local[1]!r} styx={resp!r}" + elif w.normalize(local[1]) != w.normalize(resp): + return f"result: local={w.normalize(local[1])!r} styx={w.normalize(resp)!r}" + + # compare full observable state + ls, ss = w.local_state(), w.styx_state() + if ls != ss: + diff = {k: (ls.get(k), ss.get(k)) for k in ls if ls.get(k) != ss.get(k)} + return f"state diverged (local, styx): {diff}" + return None + + def test_random_sequences(self): + coverage = Counter() + for seq in range(N_SEQUENCES): + seed = SEED + seq + rng = random.Random(seed) + w = World(rng) + trace = [] + done = 0 + while done < OPS_PER_SEQ: + gen = rng.choices([g for g, _ in GENERATORS], weights=[wt for _, wt in GENERATORS])[0] + built = gen(rng, w) + if built is None: + continue + kind, idx, method, args = built + mismatch = self._run_op(w, kind, idx, method, args) + if mismatch == "SKIP": + continue + trace.append(f"<{kind}{idx}>.{method}{args}") + done += 1 + coverage[method] += 1 + self.assertIsNone( + mismatch, f"\nSEED={seed} step {done}: {trace[-1]}\n{mismatch}\ntrace:\n " + "\n ".join(trace) + ) + print(f"seq {seq + 1}/{N_SEQUENCES} (seed {seed}): {done} ops ok") + print("\noperation coverage:") + for method, count in sorted(coverage.items()): + print(f" {method:32s} {count}") + print(f" total: {sum(coverage.values())} operations, {len(coverage)} distinct methods") + + +if __name__ == "__main__": + unittest.main() diff --git a/obol/uv.lock b/obol/uv.lock new file mode 100644 index 00000000..19d3fd58 --- /dev/null +++ b/obol/uv.lock @@ -0,0 +1,1595 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "aiokafka" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/5f/dfc1180fd22d1acdc91949ec36e97199c43742dacb057cb8efed3679ed04/aiokafka-0.14.0.tar.gz", hash = "sha256:8ffdc945798ba4d3d132b705d4244d0a1f493925efb57c637a2ca88ee82794e1", size = 601374, upload-time = "2026-04-29T10:43:03.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9d/984803315fe2b883ea6e08b1d9c8a752bd5c16e966d8714bacc67c72c417/aiokafka-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5d70615d1530ad19d0c4da8d87abaec0a12b9fdaabffdcd4e400efa0c50ef80c", size = 346672, upload-time = "2026-04-29T10:42:55.267Z" }, + { url = "https://files.pythonhosted.org/packages/49/df/da314966b7f3c3117bd78b082563cb03dbe3007848cb8f4b0932faf390a0/aiokafka-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e2392360c370b1ba6564c57d2889e154ecdb43157a8f7b7d7afe5e3c02fcc1a", size = 349594, upload-time = "2026-04-29T10:42:56.565Z" }, + { url = "https://files.pythonhosted.org/packages/57/7a/160516944ea0e0f68ea78e38f944c52f5248c7c7df26cba22a40b9f25709/aiokafka-0.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:201e38ecc595f9f65a945f1ef9085157ddf28f25cd2e482fd9efa1fcf4638213", size = 1114112, upload-time = "2026-04-29T10:42:57.869Z" }, + { url = "https://files.pythonhosted.org/packages/68/c4/9841118a2157e913e8ebfbc0a2b58f7b60f1f7202040c3e1df8925ed1184/aiokafka-0.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cd651e1f56571baae306fdd0b5509047ab9625797a24cd75902e139c5a20318", size = 1098571, upload-time = "2026-04-29T10:42:59.356Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a1/0af8a37849a4108ae227f46c4c62f6beab31863cf66ba318fb73b0be5b26/aiokafka-0.14.0-cp314-cp314-win32.whl", hash = "sha256:128127eb96dab98150b636bb5f480c80e15f02f82a118eec206a521c8cf7cf7c", size = 314107, upload-time = "2026-04-29T10:43:01.111Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/fb46c65f758900c71d0f1c73b7802720f99cabcb1f4a11676573f9bc1b8f/aiokafka-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa385039aa9b235359319bbdcf48c9c86a75d81c9c547d645056d00361238903", size = 333320, upload-time = "2026-04-29T10:43:02.424Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/34/5cd5603cf76730cac3e92c04bf7f076d5a2686fc57af8371e3f37f30ac88/boto3-1.43.32.tar.gz", hash = "sha256:15544d42af8aa8f775ea636f77c9c97fbc90ff28c0e5a0d1d47c8acd9f5b1982", size = 113169, upload-time = "2026-06-17T20:30:08.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/96/ab658327b0d628b6deb14dd0e4898557f495a7b236e5db0f437c4c2c0a38/boto3-1.43.32-py3-none-any.whl", hash = "sha256:a3d7d7a9489c18cc9c806aca9be689677779d17ddbf919fe300146576c1bdee2", size = 140533, upload-time = "2026-06-17T20:30:06.615Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.32" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/f2/3052825265c331886a92da823212ae4830471efd726ac901349d1a6b7c51/botocore-1.43.32.tar.gz", hash = "sha256:88ed52268b8f7e8ff8f9df5adbbf61e5bbb5dc618ee50de4346cabe93a482792", size = 15580749, upload-time = "2026-06-17T20:29:57.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/20/b09c65e5903dc61ebbb3e000f725403e8c3dfc7b0f4697db84b5902032d0/botocore-1.43.32-py3-none-any.whl", hash = "sha256:429796537fde1301df90d394808985d88cd51b36a6e769e5c12f6a6877605428", size = 15264015, upload-time = "2026-06-17T20:29:54.138Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "cityhash" +version = "0.4.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/29/06f572448a407cbcc6565b4568086da70552a74a850ef20c9c73f1cbbf81/cityhash-0.4.10.tar.gz", hash = "sha256:7e35da9aaf5fcf91da3fea23405874db55ffa58b1abc441d39cce0c8704a9c15", size = 274911, upload-time = "2025-10-09T21:57:51.795Z" } + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "confluent-kafka" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e5/58ac277094b03a51d9725798d0e53a60c1be69dc4330ae9cedc2d9efa430/confluent_kafka-2.14.2.tar.gz", hash = "sha256:fc827265571a778b1ff560ab2f3ec5dce3c573c29eb4cf6ded9718d55a5e4262", size = 289180, upload-time = "2026-06-03T09:53:37.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/7a/0ab7208fd86e7295a83a372c4978ab06d7caa36c925bb3c82fbcb4ff74fd/confluent_kafka-2.14.2-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:f42c745e6162905bd16bc2beb6429aef38240ff0e2f1c3edde4a462f121ea9c1", size = 4187700, upload-time = "2026-06-03T09:52:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/968c53892abaface22071b306333d83f61d8db5a1eb7224af61cde99e770/confluent_kafka-2.14.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:3d1e5f1853a67be1573014ad610874e4e80bdcfb1c7ca1cadaeae5c5bef88a94", size = 4190827, upload-time = "2026-06-03T09:52:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/99/77/722ce2b8b6e2ad0ffe9d7a042b0d0011edb679ec292203a739d10c483e26/confluent_kafka-2.14.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:c5f247f6c046e457f369d3f2b11cb8d133b17e8306ce63c2ae0414469819d3e6", size = 4803806, upload-time = "2026-06-03T09:52:32.392Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fd/a8466ed8fd6d6f82a13fd852fa14eca8b71cbca192fe82d58677808fcae3/confluent_kafka-2.14.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d61e22e6cd24e518c47b3b608023d2476bca4a7525de7877ca28e323af98f832", size = 4598793, upload-time = "2026-06-03T09:52:37.059Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/c74394c74541f4c06be482ef03e3d3f668a4df5e4b52bc50e6a43bb9c345/confluent_kafka-2.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:cf18131b6b8137f5ea0de1ff0988eb2af40a372c31f8238b015d70ccd1c0945a", size = 4584051, upload-time = "2026-06-03T09:52:42.226Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hatch" +version = "1.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "pyproject-hooks" }, + { name = "python-discovery" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/02/ce9c4c439fa3f195b21b4b5bb18b44d1076297c86477ef7e3d2de6064ec3/hatch-1.16.5.tar.gz", hash = "sha256:57bdeeaa72577859ce37091a5449583875331c06f9cb6af9077947ad40b3a1de", size = 5220741, upload-time = "2026-02-27T18:45:31.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/8a/11ae7e271870f0ad8fa0012e4265982bebe0fdc21766b161fb8b8fc3aefc/hatch-1.16.5-py3-none-any.whl", hash = "sha256:d9b8047f2cd10d3349eb6e8f278ad728a04f91495aace305c257d5c2747188fb", size = 141269, upload-time = "2026-02-27T18:45:29.573Z" }, +] + +[[package]] +name = "hatchling" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, +] + +[[package]] +name = "html5tagger" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/02/2ae5f46d517a2c1d4a17f2b1e4834c2c7cc0fb3a69c92389172fa16ab389/html5tagger-1.3.0.tar.gz", hash = "sha256:84fa3dfb49e5c83b79bbd856ab7b1de8e2311c3bb46a8be925f119e3880a8da9", size = 14196, upload-time = "2023-03-28T05:59:34.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/12/2f5d43ee912ea14a6baba4b3db6d309b02d932e3b7074c3339b4aded98ff/html5tagger-1.3.0-py3-none-any.whl", hash = "sha256:ce14313515edffec8ed8a36c5890d023922641171b4e6e5774ad1a74998f5351", size = 10956, upload-time = "2023-03-28T05:59:32.524Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "immutables" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/41/0ccaa6ef9943c0609ec5aa663a3b3e681c1712c1007147b84590cec706a0/immutables-0.21.tar.gz", hash = "sha256:b55ffaf0449790242feb4c56ab799ea7af92801a0a43f9e2f4f8af2ab24dfc4a", size = 89008, upload-time = "2024-10-10T00:55:01.434Z" } + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "libcst" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" }, + { url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" }, + { url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" }, +] + +[[package]] +name = "libcst-dfa" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "immutables" }, + { name = "libcst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/11/624a418d6ba2e686a158459dc31416ff191d94308c254c5e7219a1555898/libcst_dfa-0.0.1.tar.gz", hash = "sha256:6054255c951cf7a3813e23ae179591767ea9ae3dcadf656dd26f4336acfdfe7d", size = 87271, upload-time = "2026-06-09T14:31:22.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/39/b62afe5d64c3ee346c19e1dba56ee3ddfd9b28a6bfb6fe24835b2f4069e8/libcst_dfa-0.0.1-py3-none-any.whl", hash = "sha256:056bcec83ef8022de554a9392b5b0553b0c99a8edc99cc8dfd28612b262603e6", size = 15813, upload-time = "2026-06-09T14:31:24.081Z" }, +] + +[[package]] +name = "libcst-mypy" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "libcst" }, + { name = "mypy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/73/9c7a88b588d7479fbe016cc7f40c417a211b6eb4c70ddd415989b220ad43/libcst_mypy-0.1.0.tar.gz", hash = "sha256:b3386e062827256f64507ce3cf01835454a9830470ad6eee9b3d909f94ac707e", size = 8391, upload-time = "2023-05-03T12:03:46.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/0e3f95e2e42cf456156c9edc697d8ce9a67e7cc5e0d4e81a68b3d8726651/libcst_mypy-0.1.0-py3-none-any.whl", hash = "sha256:30313cb92a2943b992e29c657164c4ffefbc941d90010635dfa94dbe526dbea7", size = 5385, upload-time = "2023-05-03T12:03:42.211Z" }, +] + +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/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", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/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", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/76/5c202fecdc45d53e83e03a85bae70c48b6c81e9f87f0bc19a9e9c723bdc0/mkdocs_material-9.7.5.tar.gz", hash = "sha256:f76bdab532bad1d9c57ca7187b37eccf64dd12e1586909307f8856db3be384ea", size = 4097749, upload-time = "2026-03-10T15:43:22.809Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e1/e8080dcfa95cca267662a6f4afe29237452bdeb5a2a6555ac83646d21915/mkdocs_material-9.7.5-py3-none-any.whl", hash = "sha256:7cf9df2ff121fd098ff6e05c732b0be3699afca9642e2dfe4926c40eb5873eec", size = 9305251, upload-time = "2026-03-10T15:43:19.089Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/84/78243847ad9d5c21d30a2842720425b17e880d99dfe824dee11d6b2149b4/mkdocstrings_python-2.0.2.tar.gz", hash = "sha256:4a32ccfc4b8d29639864698e81cfeb04137bce76bb9f3c251040f55d4b6e1ad8", size = 199124, upload-time = "2026-02-09T15:12:01.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/31/7ee938abbde2322e553a2cb5f604cdd1e4728e08bba39c7ee6fae9af840b/mkdocstrings_python-2.0.2-py3-none-any.whl", hash = "sha256:31241c0f43d85a69306d704d5725786015510ea3f3c4bdfdb5a5731d83cdc2b0", size = 104900, upload-time = "2026-02-09T15:12:00.166Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/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", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "obol" +version = "0.4.0" +source = { editable = "." } +dependencies = [ + { name = "libcst" }, + { name = "libcst-dfa" }, + { name = "libcst-mypy" }, + { name = "mypy" }, + { name = "styx" }, +] + +[package.dev-dependencies] +dev = [ + { name = "hatch" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "prek" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "sanic" }, + { name = "setuptools-scm" }, +] + +[package.metadata] +requires-dist = [ + { name = "libcst", specifier = ">=1.8.6" }, + { name = "libcst-dfa", specifier = ">=0.0.1" }, + { name = "libcst-mypy", specifier = ">=0.1.0" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "styx", editable = "../styx-package" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "hatch", specifier = ">=1.13.0" }, + { name = "mkdocs-material", specifier = ">=9.7.5" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.3" }, + { name = "prek", specifier = ">=0.3.6" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "sanic", specifier = ">=25.12.1" }, + { name = "setuptools-scm", specifier = ">=9.2.2" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prek" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e4/983840179c652feb9793c95b88abfe4b1f1d1aed7a791b45db97241be1a0/prek-0.3.6.tar.gz", hash = "sha256:bdf5c1e13ba0c04c2f488c5f90b1fd97a72aa740dc373b17fbbfc51898fa0377", size = 378106, upload-time = "2026-03-16T08:31:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/05/157631f14fef32361a36956368a1e6559d857443d7585bc4c9225f4a4a18/prek-0.3.6-py3-none-linux_armv6l.whl", hash = "sha256:1713119cf0c390486786f4c84450ea584bcdf43979cc28e1350ec62e5d9a41ed", size = 5126301, upload-time = "2026-03-16T08:31:31.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/0918501708994d165c4bfc64c5749a263d04a08ae1196f3ad3b2e0d93b12/prek-0.3.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b68ef211fa60c53ec8866dcf38bacd8cb86b14f0e2b5491dd7a42370bee32e3e", size = 5527520, upload-time = "2026-03-16T08:31:41.948Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/0d8ed2eaea58d8a7c5a3b0129914b7a73cd1a1fc7513a1d6b1efa0ec4ce4/prek-0.3.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:327b9030c3424c9fbcdf962992288295e89afe54fa94a7e0928e2691d1d2b53d", size = 5120490, upload-time = "2026-03-16T08:31:29.808Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d5/63e21d19687816082df5bfd234f451b17858b37f500e2a8845cda1a031db/prek-0.3.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:61de3f019f5a082688654139fd9a3e03f74dbd4a09533667714d28833359114d", size = 5355957, upload-time = "2026-03-16T08:31:37.408Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0e/bb52a352e5d7dc92eaebb69aeef4e5b7cddc47c646e24fe9d6a61956b45d/prek-0.3.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bbba688c5283c8e8c907fb00f7c79fce630129f27f77cbee67e356fcfdedea8", size = 5055675, upload-time = "2026-03-16T08:31:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/34/8b/7c2a49314eb4909d50ee1c2171e00d524f9e080a5be598effbe36158d35c/prek-0.3.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dfe26bc2675114734fa626e7dc635f76e53a28fed7470ba6f32caf2f29cc21f", size = 5459285, upload-time = "2026-03-16T08:31:32.764Z" }, + { url = "https://files.pythonhosted.org/packages/70/11/86cbf205b111f93d45b5c04a61ea2cdcf12970b11277fa6a8eef1b8aaa0d/prek-0.3.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f8121060b4610411a936570ebb03b0f78c1b637c25d4914885b3bba127cb554", size = 6391127, upload-time = "2026-03-16T08:31:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d3/bae4a351b9b095e317ad294817d3dff980d73a907a0449b49a9549894a80/prek-0.3.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a38d8061caae4ffd757316b9ef65409d808ae92482386385413365bad033c26", size = 5734755, upload-time = "2026-03-16T08:31:34.387Z" }, + { url = "https://files.pythonhosted.org/packages/ea/48/5b1d6d91407e14f86daf580a93f073d00b70f4dca8ff441d40971652a38e/prek-0.3.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3d9e3b5031608657bec5d572fa45a41b6c7ddbe98f925f8240addbf57af55ea7", size = 5362190, upload-time = "2026-03-16T08:31:49.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/18/38d6ea85770bb522d3dad18e8bbe435365e1e3e88f67716c2d8c2e57a36a/prek-0.3.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a581d2903be460a236748fb3cfcb5b7dbe5b4af2409f06c0427b637676d4b78a", size = 5181858, upload-time = "2026-03-16T08:31:43.515Z" }, + { url = "https://files.pythonhosted.org/packages/3b/61/7179e9faffa3722a96fee8d9cebdb3982390410b85fc2aaeacfe49c361b5/prek-0.3.6-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:d663f1c467dccbd414ab0caa323230f33aa27797c575d98af1013866e1f83a12", size = 5023469, upload-time = "2026-03-16T08:31:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/ad/69/8a496892f8c9c898dea8cfe4917bbd58808367975132457b5ab5ac095269/prek-0.3.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cbc7f0b344432630e990a6c6dd512773fbb7253c8df3c3f78eedd80b115ed3c9", size = 5322570, upload-time = "2026-03-16T08:31:51.034Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/f174bcfd73e8337a4290cb7eaf70b37aaec228e4f5d5ec6e61e0546ee896/prek-0.3.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6ef02ce9d2389daae85f099fd4f34aa5537e3670b5e2a3174c9110ce69958c10", size = 5848197, upload-time = "2026-03-16T08:31:44.975Z" }, + { url = "https://files.pythonhosted.org/packages/65/6b/06371fa895a4ee7b7160685e4d3e5f8d3c21826f27fff8ed00334f646b46/prek-0.3.6-py3-none-win32.whl", hash = "sha256:341763a9264133a34570da53de86bbb785d7caf050bf4b077b4f2b098b48e322", size = 4852902, upload-time = "2026-03-16T08:31:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a3/63b25796e8cdaea1d62d4a82f4852cb4f52dcbad0cae465e9eabbe6acda8/prek-0.3.6-py3-none-win_amd64.whl", hash = "sha256:32803160223ecb1eefffd941804fc1175dc9376b24d10a0f03fef63dc7e10e7c", size = 5253284, upload-time = "2026-03-16T08:31:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/c031f2c6a30c921d6d3656750676c3436d9b8ada771193d36f26cd998066/prek-0.3.6-py3-none-win_arm64.whl", hash = "sha256:5003c183594e15a2d1e6a744c0ee7b1f7e28d7c2f05a1ea533e31e216b14f062", size = 5101874, upload-time = "2026-03-16T08:31:46.325Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/93a3e83bdf9322c7e21cafd092e56a4a17c4d8ef4277b6eb01af1a540a6f/python_discovery-1.1.0.tar.gz", hash = "sha256:447941ba1aed8cc2ab7ee3cb91be5fc137c5bdbb05b7e6ea62fbdcb66e50b268", size = 55674, upload-time = "2026-02-26T09:42:49.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/54/82a6e2ef37f0f23dccac604b9585bdcbd0698604feb64807dcb72853693e/python_discovery-1.1.0-py3-none-any.whl", hash = "sha256:a162893b8809727f54594a99ad2179d2ede4bf953e12d4c7abc3cc9cdbd1437b", size = 30687, upload-time = "2026-02-26T09:42:48.548Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/94/dcdaeb1713cab9c84def276cfac7388b17c7d9855bbcfe88d77e4dbafd44/s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685", size = 165171, upload-time = "2026-06-16T19:44:51.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, +] + +[[package]] +name = "sanic" +version = "25.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "html5tagger" }, + { name = "httptools" }, + { name = "multidict" }, + { name = "sanic-routing" }, + { name = "setuptools" }, + { name = "tracerite" }, + { name = "typing-extensions" }, + { name = "ujson", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, + { name = "uvloop", marker = "implementation_name == 'cpython' and sys_platform != 'win32'" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/a902611ef48d0cf942093ad297d079f76ec46133a7dc8d1181aa1ab94bd2/sanic-25.12.1.tar.gz", hash = "sha256:7b51bf608e4030f06a6c002cd08fed2872e4936527e63c9a8a97d5cd06a2b47f", size = 375519, upload-time = "2026-05-31T19:45:46.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/91/5ecd9a91374a7b2831cf183357ceeb609fdfc8b390adb5cac90a261fe1f8/sanic-25.12.1-py3-none-any.whl", hash = "sha256:3874f1fe0442e3b12ce4a3f6524f676d5d3c43839c6a4967d1b82fd8db53c0f3", size = 258466, upload-time = "2026-05-31T19:45:45.27Z" }, +] + +[[package]] +name = "sanic-routing" +version = "23.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/5c/2a7edd14fbccca3719a8d680951d4b25f986752c781c61ccf156a6d1ebff/sanic-routing-23.12.0.tar.gz", hash = "sha256:1dcadc62c443e48c852392dba03603f9862b6197fc4cba5bbefeb1ace0848b04", size = 29473, upload-time = "2023-12-31T09:28:36.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/e3/3425c9a8773807ac2c01d6a56c8521733f09b627e5827e733c5cd36b9ac5/sanic_routing-23.12.0-py3-none-any.whl", hash = "sha256:1558a72afcb9046ed3134a5edae02fc1552cff08f0fff2e8d5de0877ea43ed73", size = 25522, upload-time = "2023-12-31T09:28:35.233Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "setuptools-scm" +version = "9.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/b1/19587742aad604f1988a8a362e660e8c3ac03adccdb71c96d86526e5eb62/setuptools_scm-9.2.2.tar.gz", hash = "sha256:1c674ab4665686a0887d7e24c03ab25f24201c213e82ea689d2f3e169ef7ef57", size = 203385, upload-time = "2025-10-19T22:08:05.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl", hash = "sha256:30e8f84d2ab1ba7cb0e653429b179395d0c33775d54807fc5f1dd6671801aef7", size = 62975, upload-time = "2025-10-19T22:08:04.007Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "styx" +version = "0.0.1" +source = { editable = "../styx-package" } +dependencies = [ + { name = "aiokafka" }, + { name = "boto3" }, + { name = "cityhash" }, + { name = "cloudpickle" }, + { name = "confluent-kafka" }, + { name = "msgspec" }, + { name = "zstandard" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiokafka", specifier = ">=0.12.0,<1.0" }, + { name = "boto3", specifier = ">=1.42.59" }, + { name = "cityhash", specifier = ">=0.4.10,<1.0.0" }, + { name = "cloudpickle", specifier = ">=3.1.2,<4.0.0" }, + { name = "confluent-kafka", specifier = ">=2.14.0,<3.0.0" }, + { name = "msgspec", specifier = ">=0.20.0,<1.0.0" }, + { name = "zstandard", specifier = ">=0.25.0,<1.0.0" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tracerite" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "html5tagger" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/b9/89b065c1818e5973c333a33311f823954ff4c7c48440c20b37669c5b752c/tracerite-2.3.1.tar.gz", hash = "sha256:f46ee672d240d500a2331781b09eb33564d473f6ae60cd871ebce6c2413cffa8", size = 61303, upload-time = "2025-12-30T22:51:19.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/62/3f385a67ff3cc91209f107d20bbebdecf7a4e4aba55a43f9f71bddc424a9/tracerite-2.3.1-py3-none-any.whl", hash = "sha256:5f9595ba90f075b58e14a9baf84d8204fec3cdce50029f1c32d757af79d9ccbe", size = 65884, upload-time = "2025-12-30T22:51:18.1Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "ujson" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/89/7a/c8bb37c8f6f3623d60c33d15d18cd6d6655d0f9c3eb31a9969f76361b199/ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75", size = 7166784, upload-time = "2026-06-14T22:36:50.209Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/9a/b5139d696f5328f3cab70b9ec046f15e3f49497a4de6280974640602f539/ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b", size = 56897, upload-time = "2026-06-14T22:35:57.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/55/477183aeddfdf0f88ae039ffee0ed866cfb993da0c0c9aa915807554aef8/ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5", size = 54451, upload-time = "2026-06-14T22:35:58.932Z" }, + { url = "https://files.pythonhosted.org/packages/ea/63/55e5f23e156b4c8bca095d828b4cd3180c0b42aa3501ef88836d79606fea/ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a", size = 60053, upload-time = "2026-06-14T22:35:59.969Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/08c6cf5548bd6f4bb557c9fa7e8edf87324bb04c17249d1966028d61dde0/ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0", size = 53481, upload-time = "2026-06-14T22:36:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b3/0ac9a03551467784067f505df1bb875c639ba32f1da79ce467ab15911ada/ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71", size = 55058, upload-time = "2026-06-14T22:36:02.297Z" }, + { url = "https://files.pythonhosted.org/packages/ba/be/ec91029aec067174473d022fa0f6c3c1431a173f888d7599739f05c668eb/ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e", size = 58225, upload-time = "2026-06-14T22:36:03.321Z" }, + { url = "https://files.pythonhosted.org/packages/29/33/a948f329252ece3f9c93d177243de6e677927ebc6ac44256742dbbef3c39/ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97", size = 57930, upload-time = "2026-06-14T22:36:04.385Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0c/c33655218b8e0a8adbf066de0b999cae5c324061f3eaa4dda17423145d9e/ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285", size = 1037728, upload-time = "2026-06-14T22:36:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/d286947525ea7ce3f2d8dc55c15b9ffbe425bc455c96af7b8f8a402599a9/ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d", size = 1197146, upload-time = "2026-06-14T22:36:06.839Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3c/9eb916377050b0785f048a34588c1c390ddd41ae00b78db68ee1ad022356/ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c", size = 1090223, upload-time = "2026-06-14T22:36:08.329Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/1c543837c6a3c6672361882a0fa269bd02daf9cc4c0ca88a9dccd9df98d9/ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979", size = 57402, upload-time = "2026-06-14T22:36:13.238Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/39862f0f7174ff07cfd1e2d0c9065ded34aeebdb7db8daf2f0e5bf89b46f/ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267", size = 54973, upload-time = "2026-06-14T22:36:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/02/66/f53d3b32c3f177f846ca6b624e832f29000d8a213a2d8768e254bd470ced/ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a", size = 60683, upload-time = "2026-06-14T22:36:15.806Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d4/dddc4646d2633c85c938c2ded7d5a9711cdad5be1e13b31b7dad76f61c83/ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a", size = 54167, upload-time = "2026-06-14T22:36:16.952Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c0/d8608c3f4d3f05e6441364b63fde1d279700135c1a6577a773662c07fbcc/ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f", size = 55568, upload-time = "2026-06-14T22:36:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/22/8e/dd12b735aaba0806c3d70c18184d50e1f9712e0757c7c0a4f376450cfe28/ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1", size = 59086, upload-time = "2026-06-14T22:36:19.071Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/ad41e8752d5ec3a590a5e7b426a54e36b7aab911d9b5a4f7384dc62507ab/ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52", size = 58667, upload-time = "2026-06-14T22:36:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/b44a6afb77b94118655c029081b7932d64bb4c5b1c8ba2b7f5808b5d0bc2/ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d", size = 1038553, upload-time = "2026-06-14T22:36:21.245Z" }, + { url = "https://files.pythonhosted.org/packages/7e/93/fab1d786174c8780eb3e386c73f1925a435e97fbf77c957fea4fca83994d/ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469", size = 1197938, upload-time = "2026-06-14T22:36:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/2f073bb708f9d128f5d1cb39063a5f6421b1ce94c61be8661c55a189f407/ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8", size = 1090938, upload-time = "2026-06-14T22:36:23.947Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, +] + +[[package]] +name = "uv" +version = "0.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/ec/b324a43b55fe59577505478a396cb1d2758487a2e2270c81ccfa4ac6c96d/uv-0.10.7.tar.gz", hash = "sha256:7c3b0133c2d6bd725d5a35ec5e109ebf0d75389943abe826f3d9ea6d6667a375", size = 3922193, upload-time = "2026-02-27T12:33:58.525Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1b/decff24553325561850d70b75c737076e6fcbcfbf233011a27a33f06e4d9/uv-0.10.7-py3-none-linux_armv6l.whl", hash = "sha256:6a0af6c7a90fd2053edfa2c8ee719078ea906a2d9f4798d3fb3c03378726209a", size = 22497542, upload-time = "2026-02-27T12:33:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b5/51152c87921bc2576fecb982df4a02ac9cfd7fc934e28114a1232b99eed4/uv-0.10.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b7db0cab77232a7c8856062904fc3b9db22383f1dec7e97a9588fb6c8470f6a", size = 21558860, upload-time = "2026-02-27T12:34:03.362Z" }, + { url = "https://files.pythonhosted.org/packages/5e/15/8365dc2ded350a4ee5fcbbf9b15195cb2b45855114f2a154b5effb6fa791/uv-0.10.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d872d2ff9c9dfba989b5f05f599715bc0f19b94cd0dbf8ae4ad22f8879a66c8c", size = 20212775, upload-time = "2026-02-27T12:33:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/ccf25e897f3907b5a6fd899007ff9a80b5bbf151b3a75a375881005611fd/uv-0.10.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d9b40d03693efda80a41e5d18ac997efdf1094b27fb75471c1a8f51a9ebeffb3", size = 22015584, upload-time = "2026-02-27T12:33:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5099747954e7774768572d30917bb6bda6b8d465d7a3c49c9bbf7af2a812/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:e74fe4df9cf31fe84f20b84a0054874635077d31ce20e7de35ff0dd64d498d7b", size = 22100376, upload-time = "2026-02-27T12:34:06.169Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/75897fd966b871803cf78019fa31757ced0d54af5ffd7f57bce8b01d64f3/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c76659fc8bb618dd35cd83b2f479c6f880555a16630a454a251045c4c118ea4", size = 22105202, upload-time = "2026-02-27T12:34:16.972Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1e/0b8caedd66ca911533e18fd051da79a213c792404138812c66043d529b9e/uv-0.10.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d160cceb9468024ca40dc57a180289dfd2024d98e42f2284b9ec44355723b0a", size = 23335601, upload-time = "2026-02-27T12:34:11.161Z" }, + { url = "https://files.pythonhosted.org/packages/69/94/b741af277e39a92e0da07fe48c338eee1429c2607e7a192e41345208bb24/uv-0.10.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c775975d891cb60cf10f00953e61e643fcb9a9139e94c9ef5c805fe36e90477f", size = 24152851, upload-time = "2026-02-27T12:33:33.904Z" }, + { url = "https://files.pythonhosted.org/packages/27/b2/da351ccd02f0fb1aec5f992b886bea1374cce44276a78904348e2669dd78/uv-0.10.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a709e75583231cc1f39567fb3d8d9b4077ff94a64046eb242726300144ed1a4a", size = 23276444, upload-time = "2026-02-27T12:33:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/71/a9/2735cc9dc39457c9cf64d1ce2ba5a9a8ecbb103d0fb64b052bf33ba3d669/uv-0.10.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89de2504407dcf04aece914c6ca3b9d8e60cf9ff39a13031c1df1f7c040cea81", size = 23218464, upload-time = "2026-02-27T12:34:00.904Z" }, + { url = "https://files.pythonhosted.org/packages/20/5f/5f204e9c3f04f5fc844d2f98d80a7de64b6b304af869644ab478d909f6ff/uv-0.10.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9945de1d11c4a5ad77e9c4f36f8b5f9e7c9c3c32999b8bc0e7e579145c3b641c", size = 22092562, upload-time = "2026-02-27T12:34:14.155Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/16bebf106e3289a29cc1e1482d551c49bd220983e9b4bc5960142389ad3f/uv-0.10.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbe43527f478e2ffa420516aa465f82057763936bbea56f814fd054a9b7f961f", size = 22851312, upload-time = "2026-02-27T12:34:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/953b1da589225d98ca8668412f665c3192f6deed2a0f4bb782b0df18f611/uv-0.10.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c0783f327631141501bdc5f31dd2b4c748df7e7f5dc5cdbfc0fbb82da86cc9ca", size = 22543775, upload-time = "2026-02-27T12:33:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/e133afdabf76e43989448be1c2ef607f13afc32aa1ee9f6897115dec8417/uv-0.10.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:eba438899010522812d3497af586e6eedc94fa2b0ced028f51812f0c10aafb30", size = 23431187, upload-time = "2026-02-27T12:33:42.131Z" }, + { url = "https://files.pythonhosted.org/packages/ba/40/6ffb58ec88a33d6cbe9a606966f9558807f37a50f7be7dc756824df2d04c/uv-0.10.7-py3-none-win32.whl", hash = "sha256:b56d1818aafb2701d92e94f552126fe71d30a13f28712d99345ef5cafc53d874", size = 21524397, upload-time = "2026-02-27T12:33:44.579Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/74f4d625db838f716a555908d41777b6357bacc141ddef117a01855e5ef9/uv-0.10.7-py3-none-win_amd64.whl", hash = "sha256:ad0d0ddd9f5407ad8699e3b20fe6c18406cd606336743e246b16914801cfd8b0", size = 23999929, upload-time = "2026-02-27T12:33:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/48/4e/20cbfbcb1a0f48c5c1ca94f6baa0fa00754aafda365da9160c15e3b9c277/uv-0.10.7-py3-none-win_arm64.whl", hash = "sha256:edf732de80c1a9701180ef8c7a2fa926a995712e4a34ae8c025e090f797c2e0b", size = 22353084, upload-time = "2026-02-27T12:33:52.792Z" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/obol/working-example/Dockerfile b/obol/working-example/Dockerfile new file mode 100644 index 00000000..f03126fa --- /dev/null +++ b/obol/working-example/Dockerfile @@ -0,0 +1,30 @@ +# Build context is the styx repo root (see docker-compose-user-item.yml), so +# this image can reach both styx-package/ and obol/. +FROM python:3.14.3-slim-trixie + +RUN apt-get update && apt-get install -y --no-install-recommends \ + g++ \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd -r styx && useradd -rm -d /usr/local/styx -g styx styx + +USER styx + +ENV PATH="/usr/local/styx/.local/bin:${PATH}" +ENV PYTHONPATH="/usr/local/styx" + +# Install the demo app deps (sanic, watchdog) and the local Styx package. +COPY --chown=styx:styx obol/working-example/requirements.txt /tmp/requirements.txt +COPY --chown=styx:styx styx-package /tmp/styx-package +RUN pip install --upgrade pip && \ + pip install --user -r /tmp/requirements.txt && \ + pip install --user /tmp/styx-package + +# Preserve the obol/ layout: app.py loads ../examples/compiled/.py +# relative to its own location, so working-example and examples must keep their +# sibling relationship inside the image. +COPY --chown=styx:styx obol/examples/compiled /usr/local/styx/obol/examples/compiled +COPY --chown=styx:styx obol/working-example /usr/local/styx/obol/working-example + +WORKDIR /usr/local/styx/obol/working-example + +EXPOSE 8002 diff --git a/obol/working-example/app.py b/obol/working-example/app.py new file mode 100644 index 00000000..23d630fd --- /dev/null +++ b/obol/working-example/app.py @@ -0,0 +1,502 @@ +import importlib.util +import os +import pathlib +import sys +import uuid +from timeit import default_timer as timer + +from sanic import Sanic, json, text +from styx.client import AsyncStyxClient +from styx.client.styx_future import StyxResponse +from styx.common.local_state_backends import LocalStateBackend +from styx.common.stateflow_graph import StateflowGraph + +EXAMPLE = os.environ.get("OBOL_EXAMPLE", "user_item") +_compiled_path = pathlib.Path(__file__).resolve().parent.parent / "examples" / "compiled" / f"{EXAMPLE}.py" +_spec = importlib.util.spec_from_file_location("obol_compiled_app", _compiled_path) +_compiled = importlib.util.module_from_spec(_spec) +sys.modules["obol_compiled_app"] = _compiled +_spec.loader.exec_module(_compiled) + +item_operator = _compiled.item_operator +user_operator = _compiled.user_operator +coupon_operator = _compiled.coupon_operator +OutOfStock = _compiled.OutOfStock +NotEnoughBalance = _compiled.NotEnoughBalance + +app = Sanic("obol-app") + +STYX_HOST = os.environ.get("STYX_HOST", "localhost") +STYX_PORT = int(os.environ.get("STYX_PORT", "8888")) +KAFKA_URL = os.environ.get("KAFKA_URL", "localhost:9092") + +styx_client = AsyncStyxClient(STYX_HOST, STYX_PORT, KAFKA_URL) + + +# ── CORS ────────────────────────────────────────────────────────────────────── + + +@app.on_request +async def handle_options(request): + if request.method == "OPTIONS": + return text( + "", + status=204, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Max-Age": "86400", + }, + ) + return None + + +@app.on_response +async def add_cors(request, response): + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type" + + +# ── Lifecycle ───────────────────────────────────────────────────────────────── + + +@app.listener("before_server_start") +async def setup_styx(app, loop): + await styx_client.open(consume=True) + + +@app.get("/") +async def health_check(_): + return text("User Item App is running") + + +# ── Dataflow ────────────────────────────────────────────────────────────────── + + +@app.post("/submit/") +async def submit_dataflow_graph(_, n_partitions: str): + partitions = int(n_partitions) + g = StateflowGraph("wdm-project", operator_state_backend=LocalStateBackend.DICT) + + item_operator.set_n_partitions(partitions) + user_operator.set_n_partitions(partitions) + coupon_operator.set_n_partitions(partitions) + g.add_operators(item_operator, user_operator, coupon_operator) + + await styx_client.submit_dataflow(g) + return json({"graph_submitted": True}) + + +# ── User Endpoints ──────────────────────────────────────────────────────────── + + +@app.post("/user/create") +async def create_user(request): + body = request.json or {} + name = body.get("name", "unknown") + future = await styx_client.send_event(operator=user_operator, key=name, function="insert", params=(name,)) + result: StyxResponse = await future.get() + return json({"user_id": result.response}) + + +@app.get("/user//balance") +async def get_balance(request, user_id: str): + future = await styx_client.send_event(operator=user_operator, key=user_id, function="get_balance") + result: StyxResponse = await future.get() + return json({"balance": result.response}) + + +@app.get("/user//items") +async def get_user_items(request, user_id: str): + future = await styx_client.send_event(operator=user_operator, key=user_id, function="get_items") + result: StyxResponse = await future.get() + return json({"items": result.response}) + + +@app.post("/user//add_balance/") +async def add_balance(request, user_id: str, amount: str): + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="add_balance", params=(int(amount),) + ) + result: StyxResponse = await future.get() + return json({"success": result.response}) + + +# ── Item Endpoints ──────────────────────────────────────────────────────────── + + +@app.post("/item/create") +async def create_item(request): + body = request.json or {} + name = body.get("name", str(uuid.uuid4())) + price = int(body.get("price", 0)) + future = await styx_client.send_event(operator=item_operator, key=name, function="insert", params=(name, price)) + result: StyxResponse = await future.get() + return json({"item_id": result.response}) + + +@app.get("/item//stock") +async def get_stock(request, item_id: str): + future = await styx_client.send_event(operator=item_operator, key=item_id, function="get_stock") + result: StyxResponse = await future.get() + return json({"stock": result.response}) + + +@app.post("/item//add_stock/") +async def add_stock(request, item_id: str, amount: str): + future = await styx_client.send_event( + operator=item_operator, key=item_id, function="update_stock", params=(int(amount),) + ) + result: StyxResponse = await future.get() + return json({"success": result.response}) + + +@app.get("/item//price") +async def get_price(request, item_id: str): + future = await styx_client.send_event(operator=item_operator, key=item_id, function="get_price") + result: StyxResponse = await future.get() + return json({"price": result.response}) + + +# ── Coupon Endpoints ────────────────────────────────────────────────────────── + + +@app.post("/coupon/create") +async def create_coupon(request): + body = request.json or {} + code = body.get("code", str(uuid.uuid4())) + discount = int(body.get("discount", 0)) + future = await styx_client.send_event( + operator=coupon_operator, key=code, function="insert", params=(code, discount) + ) + result: StyxResponse = await future.get() + return json({"coupon_id": result.response}) + + +@app.get("/coupon//discount") +async def get_discount(request, coupon_id: str): + future = await styx_client.send_event(operator=coupon_operator, key=coupon_id, function="get_discount") + result: StyxResponse = await future.get() + return json({"discount": result.response}) + + +# ── Transaction Endpoints ───────────────────────────────────────────────────── + + +@app.post("/buy_item///") +async def buy_item(request, user_id: str, item_id: str, amount: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="buy_item", params=(int(amount), item_id) + ) + result: StyxResponse = await future.get() + return json({"purchase_successful": result.response, "latency": result.styx_latency_ms}) + except (OutOfStock, NotEnoughBalance) as e: + return json({"purchase_successful": False, "error": str(e)}, status=400) + + +@app.post("/user//transfer//") +async def transfer_balance(request, user_id: str, recipient_id: str, amount: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="transfer_balance", params=(recipient_id, int(amount)) + ) + result: StyxResponse = await future.get() + return json({"success": result.response}) + except NotEnoughBalance as e: + return json({"success": False, "error": str(e)}, status=400) + + +@app.get("/user//is_in_stock/") +async def is_in_stock(request, user_id: str, item_id: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="is_in_stock", params=(item_id,) + ) + result: StyxResponse = await future.get() + return json({"is_in_stock": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.get("/user//discounted_price//") +async def get_discounted_price(request, user_id: str, item_id: str, coupon_id: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="get_discounted_price", params=(item_id, coupon_id) + ) + result: StyxResponse = await future.get() + return json({"discounted_price": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.post("/user//gather_in_loop") +async def gather_in_loop(request, user_id: str): + body = request.json or {} + items: list[str] = body.get("items", []) + coupons: list[str] = body.get("coupons", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="gather_in_loop", params=(items, coupons) + ) + result: StyxResponse = await future.get() + return json({"total": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.post("/user//buy_with_coupon/") +async def buy_with_coupon(request, user_id: str, item_id: str): + body = request.json or {} + coupon = body.get("coupon") # may be None + try: + start = timer() + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="buy_with_coupon", params=(item_id, coupon) + ) + result: StyxResponse = await future.get() + end = timer() + c_lat = round((end - start) * 1000, 0) + return json( + { + "purchase_successful": result.response, + "latency": result.styx_latency_ms, + "total_time": c_lat, + "client_added_latency": c_lat - result.styx_latency_ms, + } + ) + except (OutOfStock, NotEnoughBalance) as e: + return json({"purchase_successful": False, "error": str(e)}, status=400) + + +# ── Bulk / cart endpoints ───────────────────────────────────────────────────── + + +@app.post("/bulk_purchase_with_tiers/") +async def bulk_purchase_with_tiers(request, user_id: str): + body = request.json or {} + cart: list[str] = body.get("cart", []) + quantities: list[int] = body.get("quantities", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="bulk_purchase_with_tiers", params=(cart, quantities) + ) + result: StyxResponse = await future.get() + return json({"success": result.response, "latency": result.styx_latency_ms}) + except (OutOfStock, NotEnoughBalance) as e: + return json({"success": False, "error": str(e)}, status=400) + + +@app.post("/user//process_cart_with_limits") +async def process_cart_with_limits(request, user_id: str): + body = request.json or {} + cart: list[str] = body.get("cart", []) + max_spend = int(body.get("max_spend", 0)) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="process_cart_with_limits", params=(cart, max_spend) + ) + result: StyxResponse = await future.get() + return json({"purchased": result.response}) + except (OutOfStock, NotEnoughBalance) as e: + return json({"purchased": {}, "error": str(e)}, status=400) + + +@app.post("/user//can_afford_cart") +async def can_afford_cart(request, user_id: str): + body = request.json or {} + items: list[str] = body.get("items", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="can_afford_cart", params=(items,) + ) + result: StyxResponse = await future.get() + return json({"can_afford": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.post("/user//multi_restock") +async def multi_restock(request, user_id: str): + body = request.json or {} + items: list[str] = body.get("items", []) + amounts: list[int] = body.get("amounts", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="multi_restock", params=(items, amounts) + ) + result: StyxResponse = await future.get() + return json({"total_added": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.post("/user//group_items_by_price_bucket") +async def group_items_by_price_bucket(request, user_id: str): + body = request.json or {} + items: list[str] = body.get("items", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="group_items_by_price_bucket", params=(items,) + ) + result: StyxResponse = await future.get() + return json({"buckets": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.post("/recursion/") +async def recursion_test(request, user_id: str): + body = request.json or {} + items = body.get("items", []) + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="recursion_test", params=(items,) + ) + result: StyxResponse = await future.get() + return json({"recursion_test": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +# ── Read-only / utility endpoints ───────────────────────────────────────────── + + +@app.get("/inventory_value/") +async def inventory_value(request, user_id: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="inventory_value", params=() + ) + result: StyxResponse = await future.get() + return json({"inventory_value": result.response, "latency": result.styx_latency_ms}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.get("/inventory_value_gather/") +async def inventory_value_gather(request, user_id: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="inventory_value_gather", params=() + ) + result: StyxResponse = await future.get() + return json({"inventory_value_gather": result.response, "latency": result.styx_latency_ms}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.get("/my_item_prices/") +async def my_item_prices(request, user_id: str): + try: + future = await styx_client.send_event(operator=user_operator, key=user_id, function="my_item_prices", params=()) + result: StyxResponse = await future.get() + return json({"my_item_prices": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +@app.get("/user//most_valuable_item_price") +async def most_valuable_item_price(request, user_id: str): + try: + future = await styx_client.send_event( + operator=user_operator, key=user_id, function="most_valuable_item_price", params=() + ) + result: StyxResponse = await future.get() + return json({"most_valuable_item_price": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +# ── Item bulk create (itemidx removed) ─────────────────────────────────────── + +# Commented out: depended on the itemidx operator (Phase 0 created the index), +# which no longer exists. Bulk item/stock insertion is preserved here for +# reference; re-enable once a replacement for the index is decided. +# @app.post('/items/batch_create') +# async def batch_create_items(request): +# """Create the item index, insert items with prices, and set their stock. +# +# Body: {names, prices, stocks, idx_id} +# names: list[str] e.g. ["widget","gadget","thingy"] +# prices: list[int] parallel to names +# stocks: list[int] parallel to names (0 stocks are skipped) +# idx_id: str index key to create (default "items") +# +# Call this first on a fresh deployment. Phase 0 creates the index, and +# item.insert auto-registers each item into the "items" index, so the +# itemidx queries work afterwards. NOTE: item.insert hardcodes the index +# name "items", so leave idx_id at its default unless the compiled +# functions are changed to match. +# """ +# body = request.json or {} +# names: List[str] = body.get('names', []) +# prices: List[int] = [int(p) for p in body.get('prices', [])] +# stocks: List[int] = [int(s) for s in body.get('stocks', [])] +# idx_id: str = body.get('idx_id', 'items') +# +# if not (len(names) == len(prices) == len(stocks)): +# return json({'error': 'names, prices, stocks must have the same length', +# 'lengths': {'names': len(names), 'prices': len(prices), 'stocks': len(stocks)}}, +# status=400) +# +# start = timer() +# +# # Phase 0: create the item index. Must fully complete before any +# # item.insert: insert fans out to itemidx.add_item, which appends to +# # __state__['items'] and would KeyError if the index does not exist yet. +# idx_future = await styx_client.send_event( +# operator=itemidx_operator, key=idx_id, +# function='insert', params=(idx_id,) +# ) +# await idx_future.get() +# +# # Phase 1: insert all items. item.insert already registers each item in +# # the "items" index, so no separate add_item pass is needed (a Phase 3 +# # add_item loop would double-register every item). +# futures = [] +# for name, price in zip(names, prices): +# f = await styx_client.send_event( +# operator=item_operator, key=name, +# function='insert', params=(name, price) +# ) +# futures.append(f) +# for f in futures: +# await f.get() +# +# # Phase 2: set stock for items with stock > 0 +# futures = [] +# for name, stock in zip(names, stocks): +# if stock > 0: +# f = await styx_client.send_event( +# operator=item_operator, key=name, +# function='update_stock', params=(stock,) +# ) +# futures.append(f) +# for f in futures: +# await f.get() +# +# end = timer() +# return json({ +# 'count': len(names), +# 'idx_id': idx_id, +# 'items': [{'name': n, 'price': p, 'stock': s} for n, p, s in zip(names, prices, stocks)], +# 'total_time_ms': round((end - start) * 1000, 1), +# }) + + +@app.get("/demo/") +async def demo(request, user_id: str): + try: + future = await styx_client.send_event(operator=user_operator, key=user_id, function="demo", params=()) + result: StyxResponse = await future.get() + return json({"demo": result.response}) + except Exception as e: + return json({"error": str(e)}, status=400) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8002, debug=True) diff --git a/obol/working-example/docker-compose-user-item.yml b/obol/working-example/docker-compose-user-item.yml new file mode 100644 index 00000000..a1330c98 --- /dev/null +++ b/obol/working-example/docker-compose-user-item.yml @@ -0,0 +1,21 @@ +name: styx + +services: + + obol-user-item: + build: + context: ../.. + dockerfile: obol/working-example/Dockerfile + image: dev/styx-wdm-demo-async:latest + command: watchmedo auto-restart --recursive --patterns="*.py" --directory="." --debug-force-polling -- python app.py + environment: + - STYX_HOST=coordinator + - STYX_PORT=8888 + - KAFKA_URL=kafka1:19092 + - OBOL_EXAMPLE=user_item + - PYTHONUNBUFFERED=TRUE + ports: + - "8002:8002" + volumes: + - ./:/usr/local/styx/obol/working-example + - ../examples/compiled:/usr/local/styx/obol/examples/compiled diff --git a/obol/working-example/requirements.txt b/obol/working-example/requirements.txt new file mode 100644 index 00000000..310728f1 --- /dev/null +++ b/obol/working-example/requirements.txt @@ -0,0 +1,2 @@ +sanic==25.12.0 +watchdog \ No newline at end of file diff --git a/obol/working-example/user_item_api_panel.html b/obol/working-example/user_item_api_panel.html new file mode 100644 index 00000000..a5a10dcb --- /dev/null +++ b/obol/working-example/user_item_api_panel.html @@ -0,0 +1,569 @@ + + + + +Styx API Test Panel + + + +
+
+
+

Styx API test panel

+ obol-app +
+ +
+ + +
+ +
+ + + +
+ +
+ +
+
+ +
+
+

Response log

+ idle + +
+
+
No requests yet. Pick an endpoint to start.
+
+
+
+ + + + diff --git a/styx-package/styx/common/stateful_function.py b/styx-package/styx/common/stateful_function.py index b491cc2b..df654a18 100644 --- a/styx-package/styx/common/stateful_function.py +++ b/styx-package/styx/common/stateful_function.py @@ -5,7 +5,11 @@ from styx.common.logging import logging from styx.common.message_types import MessageType from styx.common.run_func_payload import RunFuncPayload -from styx.common.serialization import Serializer +from styx.common.serialization import ( + Serializer, + pickle_deserialization, + pickle_serialization, +) if TYPE_CHECKING: from collections.abc import Awaitable @@ -173,6 +177,10 @@ def key(self) -> K: """The key for this function instance.""" return self.__key + @property + def t_id(self) -> int: + return self.__t_id + async def run(self, *args) -> None: # noqa: ANN002 """Executes the actual function logic. Meant to be overridden by subclasses. @@ -229,6 +237,63 @@ def put(self, value: V) -> None: self.__partition, ) + def _func_context_key(self) -> tuple[str, K]: + return ("__func_ctx__", self.__key) + + def get_func_context(self) -> V: + """Retrieves the function context for the current key. + + Works like get() but stores data in a separate namespace, + so it never conflicts with the entity state. + """ + fc_key = self._func_context_key() + if self.__fallback_enabled: + value = self.__state.get_immediate( + fc_key, + self.__t_id, + self.__operator_name, + self.__partition, + ) + else: + value = self.__state.get( + fc_key, + self.__t_id, + self.__operator_name, + self.__partition, + ) + # Func context is stored as pickled bytes (see put_func_context) so it + # can carry arbitrary Python objects through the msgpack-based snapshot + # and migration paths. Entity state (get/put) still uses msgpack. + if value is None: + return value + return pickle_deserialization(value) + + def put_func_context(self, value: V) -> None: + """Stores a function context value for the current key. + + Works like put() but stores data in a separate namespace, + so it never conflicts with the entity state. + """ + fc_key = self._func_context_key() + + serialized_value = pickle_serialization(value) + if self.__fallback_enabled: + self.__state.put_immediate( + fc_key, + serialized_value, + self.__t_id, + self.__operator_name, + self.__partition, + ) + else: + self.__state.put( + fc_key, + serialized_value, + self.__t_id, + self.__operator_name, + self.__partition, + ) + def batch_insert(self, kv_pairs: dict) -> None: if kv_pairs: self.__state.batch_insert(kv_pairs, self.__operator_name, self.__partition) diff --git a/styx-package/styx/local_runner/local_state.py b/styx-package/styx/local_runner/local_state.py index 48fec81f..eed62019 100644 --- a/styx-package/styx/local_runner/local_state.py +++ b/styx-package/styx/local_runner/local_state.py @@ -16,6 +16,7 @@ class LocalOperatorState: def __init__(self) -> None: self._data: dict[tuple[str, int], dict[K, V]] = {} + self._func_context: dict[tuple[str, int], dict[K, V]] = {} self._key_locks: dict[tuple[str, K], asyncio.Lock] = {} def key_lock(self, operator_name: str, key: K) -> asyncio.Lock: @@ -30,6 +31,12 @@ def key_lock(self, operator_name: str, key: K) -> asyncio.Lock: def init_partition(self, operator_name: str, partition: int) -> None: self._data.setdefault((operator_name, partition), {}) + def get_func_context(self, operator_name: str, partition: int) -> dict[K, V]: + return self._func_context.get((operator_name, partition), {}) + + def put_func_context(self, operator_name: str, partition: int, context: dict[K, V]) -> None: + self._func_context[(operator_name, partition)] = context + def get(self, key: K, operator_name: str, partition: int) -> V: return self._data[(operator_name, partition)].get(key)