From 4ee54b6880c6f0cdc856b758d9d6963e654ac82b Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 13:05:06 +0200 Subject: [PATCH 1/5] Obol --- docker-compose-combined.yml | 249 ++ obol/LICENSE | 202 + obol/README.md | 142 + obol/docs/index.md | 3 + obol/examples/benchmarks/compiled/tpcc.py | 771 ++++ .../benchmarks/compiled/tpcc_no_gather.py | 789 ++++ obol/examples/benchmarks/compiled/ycsb.py | 143 + .../benchmarks/figures/tpcc_results.png | Bin 0 -> 160444 bytes .../benchmarks/figures/ycsb_results.png | Bin 0 -> 108907 bytes obol/examples/benchmarks/original/tpcc.py | 679 ++++ .../benchmarks/original/tpcc_no_gather.py | 677 ++++ obol/examples/benchmarks/original/ycsb.py | 39 + obol/examples/benchmarks/plots_tpcc.py | 193 + obol/examples/benchmarks/plots_ycsb.py | 204 + obol/examples/compiled/marketplace.py | 3477 +++++++++++++++++ obol/examples/compiled/user_item.py | 1184 ++++++ obol/examples/original/marketplace.py | 929 +++++ obol/examples/original/user_item.py | 315 ++ obol/mkdocs.yml | 15 + obol/pyproject.toml | 124 + obol/src/__init__.py | 0 obol/src/obol/__init__.py | 47 + obol/src/obol/api.py | 81 + obol/src/obol/comprehension_expander.py | 283 ++ obol/src/obol/config.py | 1 + obol/src/obol/core.py | 542 +++ obol/src/obol/cst_helpers.py | 221 ++ obol/src/obol/entity_resolver.py | 208 + obol/src/obol/liveness.py | 138 + obol/src/obol/predicates.py | 95 + obol/src/obol/processor.py | 60 + obol/src/obol/send_async.py | 127 + obol/src/obol/splitting/__init__.py | 3 + obol/src/obol/splitting/context.py | 208 + obol/src/obol/splitting/gather.py | 289 ++ obol/src/obol/splitting/if_split.py | 75 + obol/src/obol/splitting/loop.py | 266 ++ obol/src/obol/splitting/remote_call.py | 102 + obol/src/obol/transformers/__init__.py | 15 + obol/src/obol/transformers/annotations.py | 65 + obol/src/obol/transformers/linearize.py | 287 ++ obol/src/obol/transformers/normalize.py | 29 + obol/src/obol/transformers/return_handler.py | 152 + obol/src/obol/transformers/short_circuit.py | 239 ++ obol/src/obol/transformers/state_access.py | 143 + obol/src/obol/visitor.py | 66 + obol/tests/__init__.py | 3 + obol/tests/test_comprehension_expander.py | 83 + obol/tests/test_differential.py | 420 ++ obol/uv.lock | 1595 ++++++++ obol/working-example/Dockerfile | 30 + obol/working-example/app.py | 578 +++ .../docker-compose-user-item.yml | 21 + obol/working-example/requirements.txt | 2 + obol/working-example/user_item_api_panel.html | 569 +++ styx-package/styx/common/stateful_function.py | 68 +- styx-package/styx/local_runner/local_state.py | 7 + 57 files changed, 17252 insertions(+), 1 deletion(-) create mode 100644 docker-compose-combined.yml create mode 100644 obol/LICENSE create mode 100644 obol/README.md create mode 100644 obol/docs/index.md create mode 100644 obol/examples/benchmarks/compiled/tpcc.py create mode 100644 obol/examples/benchmarks/compiled/tpcc_no_gather.py create mode 100644 obol/examples/benchmarks/compiled/ycsb.py create mode 100644 obol/examples/benchmarks/figures/tpcc_results.png create mode 100644 obol/examples/benchmarks/figures/ycsb_results.png create mode 100644 obol/examples/benchmarks/original/tpcc.py create mode 100644 obol/examples/benchmarks/original/tpcc_no_gather.py create mode 100644 obol/examples/benchmarks/original/ycsb.py create mode 100644 obol/examples/benchmarks/plots_tpcc.py create mode 100644 obol/examples/benchmarks/plots_ycsb.py create mode 100644 obol/examples/compiled/marketplace.py create mode 100644 obol/examples/compiled/user_item.py create mode 100644 obol/examples/original/marketplace.py create mode 100644 obol/examples/original/user_item.py create mode 100644 obol/mkdocs.yml create mode 100644 obol/pyproject.toml create mode 100644 obol/src/__init__.py create mode 100644 obol/src/obol/__init__.py create mode 100644 obol/src/obol/api.py create mode 100644 obol/src/obol/comprehension_expander.py create mode 100644 obol/src/obol/config.py create mode 100644 obol/src/obol/core.py create mode 100644 obol/src/obol/cst_helpers.py create mode 100644 obol/src/obol/entity_resolver.py create mode 100644 obol/src/obol/liveness.py create mode 100644 obol/src/obol/predicates.py create mode 100644 obol/src/obol/processor.py create mode 100644 obol/src/obol/send_async.py create mode 100644 obol/src/obol/splitting/__init__.py create mode 100644 obol/src/obol/splitting/context.py create mode 100644 obol/src/obol/splitting/gather.py create mode 100644 obol/src/obol/splitting/if_split.py create mode 100644 obol/src/obol/splitting/loop.py create mode 100644 obol/src/obol/splitting/remote_call.py create mode 100644 obol/src/obol/transformers/__init__.py create mode 100644 obol/src/obol/transformers/annotations.py create mode 100644 obol/src/obol/transformers/linearize.py create mode 100644 obol/src/obol/transformers/normalize.py create mode 100644 obol/src/obol/transformers/return_handler.py create mode 100644 obol/src/obol/transformers/short_circuit.py create mode 100644 obol/src/obol/transformers/state_access.py create mode 100644 obol/src/obol/visitor.py create mode 100644 obol/tests/__init__.py create mode 100644 obol/tests/test_comprehension_expander.py create mode 100644 obol/tests/test_differential.py create mode 100644 obol/uv.lock create mode 100644 obol/working-example/Dockerfile create mode 100644 obol/working-example/app.py create mode 100644 obol/working-example/docker-compose-user-item.yml create mode 100644 obol/working-example/requirements.txt create mode 100644 obol/working-example/user_item_api_panel.html 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/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/docs/index.md b/obol/docs/index.md new file mode 100644 index 00000000..e94b6a22 --- /dev/null +++ b/obol/docs/index.md @@ -0,0 +1,3 @@ +# obol +::: obol + show_root: true(base) 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 0000000000000000000000000000000000000000..50cbbdd13ae7771cf19d9c05e799338dca1b269c GIT binary patch literal 160444 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

YfVet(qJyVI-bh$fUs!@)R)EEPWG~ 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�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

    @-ErQ*o6Pjvl}vUKxab8HA8N%fZh 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 h(aCIXx`^@lyK2x2|9= zPLGjd-Zy<^z?I_WCB%8$4r-}-hiTB)wFYF@`dw}m!4E?e)CqW33f+u;J+Zu^V+}Jv z-EnS%cmp(@cL854I(dL(al;}*r}-0|sQWA5b)e)b`c@=@B_>J{<6$oddvukpK9;M| z<90RTWuASS4KQ&sQ)VC}&wV1pRMbBJYQpeQJaHy#=R_gJENCuF#e-#Lx>LmU8;)j3 zmVq!l3gtDg8%`YFE_7%GUethehF=WmYw!BQ)4*JVlU%p~S4(YCoZ*k&6cn)Q)_YaB zHox-VuNmzzQzwqt-;gcKfVA8&^NL)O5tALQ$P=lo*s+7OT_CM2-b zTjGgK2)U<0FB3n1#h8>@8s3GC?o`R?WA9qRs|IrTUg*if1M(#=lp9V=d3iZ?<6FXb zzNK^!K_jjEOo4V}AxZjJ9GDxfKSc{xJ4}jLgQ!Uh8B)`%Y}^}A#D<)X77F7v=k?o$ zqOCL5;*`m1rUVfNU6+8-@&@|a8MclAus;AU3yg)2yTJ`>&3|hM+e5YOo2j9`3hj3D zCXXiw*xNYROx1a69t@v19?gVE%tH@u7H_wXO(*!Op$bT*MMAz0wj4&YX?J$V^Y-*o ze|9od&s2=%T<`;UY~iuDIX3spbIq?;@TYBIg~m~s`8fc)31|{1sAM|<=L2Mn&8`G~ z9=D}u)|2oGSAAeZOP2Jgjg}EpL!mA;+G`=Itb%Z5^EVP0MDgDsPEH!|ToCnB>cN{QAUf9!5Z+zW+ zFIPCv${w#8pFe0kL+4Yq6xnJT03mrHs5}~x&qF$=LlLvs9nTmaA8%o7tQ(jHh_zjj z1hPU-3ObbTW8~&JmIA;Qidgp9JRA27@V0aqpY2dYzEzrDl>~OCODFm#c~~FOh4S*t zZUSAvbj1jy+3dDb(d{2M%z>$GVG%$=Ei7htrl?Cm#rO)NQvuI8jdElNWJYGVx0RN*CZxg_)l@ zwcOHfFE#HnoB8qKRuCC%?=i-?jn2~V417*=Q1n_HyszTdFj(G=HVS9}t*~8u}uU|>pV7%T(24WXY(S2 zz8eUkTncHMoUwYR&|nIdL1R{qZm4V^Dr|f4{jG4YC2aNNLIg4v#<7F-L#4e&V&*y` zl}%*Ehd|}Ok%Vj3zi1mEiwf(fQp3V_;V_sp=xXIA@DV!cVyNFb3|QA>UhO?0y-g94 zc5M-Lxk8Kw;Opz|LQ01?k@1bD1R=fbc=~R^Ih63fUJZO z#_|13XZ^`{J3B+Yf-c$V8!?Z=gb>z5DU84>C7t6p@|A$=M*GeZ$P_;TO`d>-v)^uE zF5)!*jsWkCmWIopV;PFQwb8)~3Vg597|-_~HzEd{1l1qOjbO`W3uGvie*jZ14W+0v z_B%G`6>XwP9_Dsm(J5Z+Qpat*F*-tpY{}6&zn1?B%}vMSztODiwXJXYrVf~z zpap!S@n`h#%6CRhD3gydy8bc7|@=8j4B+a&;6+yEd`R$`IuWv9& z1}<)WJM0{VIoVq_#lu6V6x&N!;z{KLs=enl30_M0?D&M2A8vlX;oDng*(<<1fF+?iWzJz095z?w z{27ux&3Y8_l<-xl_tdDH0By5H@YfKh14<7kh_&;qZZ*_a5pz_U z1lPjMF(|~%u=&936{xSU2H3@mOoqL3sF`Ml^0k6J8+fH!CLrKYnSdqFOwK3-AMrI8 z`dWpwWcpoz^L>9H4*?C=1jF{q1A<^9wp10cAG-%Eqb?_6hF4^Qxt*r3VSw8L%RBJk z767x0tQoiURaHw}I*=4yz_pQw<6vH-u7Ys`C<&x=96(Ehk*G)ujKOBVGf;rdY_AR*i50>k*Qnx7CYnI~{Kbw8EF-m@UM zXE|(IH4#)JTR;An_x$#K9RPa%di)>k`~T1E?B}1y{=-53##8F@N7&2D%cwkoJvIsY EAB{%Cj{pDw literal 0 HcmV?d00001 diff --git a/obol/examples/benchmarks/figures/ycsb_results.png b/obol/examples/benchmarks/figures/ycsb_results.png new file mode 100644 index 0000000000000000000000000000000000000000..e0c4d636d8f7e95557483a24ec2eb6f5f291a48b GIT binary patch literal 108907 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

    ~59$d-lRlZI~J;1fm0 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 literal 0 HcmV?d00001 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/mkdocs.yml b/obol/mkdocs.yml new file mode 100644 index 00000000..23108f90 --- /dev/null +++ b/obol/mkdocs.yml @@ -0,0 +1,15 @@ +site_name: Documentation + +nav: + - Home: index.md + +theme: material + +plugins: + - autorefs: + resolve_closest: true + - mkdocstrings: + handlers: + python: + options: + show_submodules: true diff --git a/obol/pyproject.toml b/obol/pyproject.toml new file mode 100644 index 00000000..6140170d --- /dev/null +++ b/obol/pyproject.toml @@ -0,0 +1,124 @@ +[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.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 = [] +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 +] + +[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..c408e9fe --- /dev/null +++ b/obol/tests/test_differential.py @@ -0,0 +1,420 @@ +""" +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. +""" + +from time import sleep +from collections import Counter +import copy +import importlib.util +import os +import random +import socket +import unittest +import uuid +import sys +import pathlib + +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: # noqa: BLE001 + 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}\n" + f"trace:\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, " + f"{len(coverage)} distinct methods") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file 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..6a4a9a4f --- /dev/null +++ b/obol/working-example/app.py @@ -0,0 +1,578 @@ +import importlib.util +import os +import pathlib +import sys +import uuid +from typing import List +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 +from timeit import default_timer as timer + +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", + }, + ) + + +@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) \ No newline at end of file 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 + + + +

    + + + + diff --git a/styx-package/styx/common/stateful_function.py b/styx-package/styx/common/stateful_function.py index b491cc2b..f2338cb9 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,64 @@ 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) From 260e0370a33566961f3ec0163efdc5cad3382447 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 13:41:31 +0200 Subject: [PATCH 2/5] Documentation and format updates --- docs/obol/index.md | 49 +++++ mkdocs.yml | 3 +- obol/docs/index.md | 3 - obol/mkdocs.yml | 15 -- obol/pyproject.toml | 6 + obol/tests/test_differential.py | 261 +++++++++++++--------- obol/working-example/app.py | 375 +++++++++++++------------------- 7 files changed, 360 insertions(+), 352 deletions(-) create mode 100644 docs/obol/index.md delete mode 100644 obol/docs/index.md delete mode 100644 obol/mkdocs.yml 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/docs/index.md b/obol/docs/index.md deleted file mode 100644 index e94b6a22..00000000 --- a/obol/docs/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# obol -::: obol - show_root: true(base) diff --git a/obol/mkdocs.yml b/obol/mkdocs.yml deleted file mode 100644 index 23108f90..00000000 --- a/obol/mkdocs.yml +++ /dev/null @@ -1,15 +0,0 @@ -site_name: Documentation - -nav: - - Home: index.md - -theme: material - -plugins: - - autorefs: - resolve_closest: true - - mkdocstrings: - handlers: - python: - options: - show_submodules: true diff --git a/obol/pyproject.toml b/obol/pyproject.toml index 6140170d..35df20fe 100644 --- a/obol/pyproject.toml +++ b/obol/pyproject.toml @@ -32,6 +32,12 @@ dev = [ 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"] diff --git a/obol/tests/test_differential.py b/obol/tests/test_differential.py index c408e9fe..2f0c8eab 100644 --- a/obol/tests/test_differential.py +++ b/obol/tests/test_differential.py @@ -12,24 +12,24 @@ 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 +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. """ -from time import sleep -from collections import Counter import copy import importlib.util import os +import pathlib import random import socket +import sys import unittest import uuid -import sys -import pathlib +from collections import Counter +from time import sleep from styx.client.sync_client import SyncStyxClient from styx.common.local_state_backends import LocalStateBackend @@ -88,7 +88,7 @@ def setUpModule(): 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) + 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) @@ -100,7 +100,7 @@ def setUpModule(): print("[setup] opening client (starting Kafka consumers)...", flush=True) styx.open(consume=True) print("[setup] ready.", flush=True) - except Exception as e: # noqa: BLE001 + except Exception as e: raise unittest.SkipTest(f"Could not connect to a running Styx deployment: {e}") from e @@ -113,17 +113,22 @@ def tearDownModule(): # 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}>" + + 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.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)) @@ -133,43 +138,50 @@ def __init__(self, rng): 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 + 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,)) + 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) + 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)) + 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) + 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) + 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] + 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"" + 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): @@ -184,18 +196,20 @@ def resolve_styx(self, 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, (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] + 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()]) + 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): @@ -205,14 +219,12 @@ def local_state(self): 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]) + 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')) + 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') + s[f""] = self._send(coupon_operator, key, "get_discount") return s @@ -221,22 +233,25 @@ def styx_state(self): # 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))] + 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))] + 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,) + 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),) + 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)) @@ -244,95 +259,129 @@ def g_buy_item(rng, w): 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)) + 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)),) + 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) + 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)) + 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) + 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)]) + 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, ()) + 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),)) + 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))),)) + 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)) + 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,) + 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)))) + 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) + 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)) + 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)))) + 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_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), + (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 @@ -345,32 +394,33 @@ def g_price_check(rng, w): # The test # -------------------------------------------------------------------------- -class TestDifferential(unittest.TestCase): +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])) + 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)) + local = ("exc", str(e)) except Exception: w.users, w.items, w.coupons = snapshot - return 'SKIP' # generator produced an ill-formed op; discard + return "SKIP" # generator produced an ill-formed op; discard # styx - op = {'user': user_operator, 'item': item_operator, - 'coupon': coupon_operator}[kind] + 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 + 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 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): @@ -392,29 +442,26 @@ def test_random_sequences(self): trace = [] done = 0 while done < OPS_PER_SEQ: - gen = rng.choices([g for g, _ in GENERATORS], - weights=[wt for _, wt in GENERATORS])[0] + 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': + 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}\n" - f"trace:\n " + "\n ".join(trace)) + 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, " - f"{len(coverage)} distinct methods") + print(f" total: {sum(coverage.values())} operations, {len(coverage)} distinct methods") if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/obol/working-example/app.py b/obol/working-example/app.py index 6a4a9a4f..20656f1d 100644 --- a/obol/working-example/app.py +++ b/obol/working-example/app.py @@ -3,13 +3,13 @@ import pathlib import sys import uuid -from typing import List +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 -from timeit import default_timer as timer EXAMPLE = os.environ.get("OBOL_EXAMPLE", "user_item") _compiled_path = pathlib.Path(__file__).resolve().parent.parent / "examples" / "compiled" / f"{EXAMPLE}.py" @@ -26,15 +26,16 @@ 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_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": @@ -59,22 +60,24 @@ async def add_cors(request, response): # ── Lifecycle ───────────────────────────────────────────────────────────────── -@app.listener('before_server_start') + +@app.listener("before_server_start") async def setup_styx(app, loop): await styx_client.open(consume=True) -@app.get('/') +@app.get("/") async def health_check(_): return text("User Item App is running") # ── Dataflow ────────────────────────────────────────────────────────────────── -@app.post('/submit/') + +@app.post("/submit/") async def submit_dataflow_graph(_, n_partitions: str): partitions = int(n_partitions) - g = StateflowGraph('wdm-project', operator_state_backend=LocalStateBackend.DICT) + g = StateflowGraph("wdm-project", operator_state_backend=LocalStateBackend.DICT) item_operator.set_n_partitions(partitions) user_operator.set_n_partitions(partitions) @@ -82,404 +85,329 @@ async def submit_dataflow_graph(_, n_partitions: str): g.add_operators(item_operator, user_operator, coupon_operator) await styx_client.submit_dataflow(g) - return json({'graph_submitted': True}) + return json({"graph_submitted": True}) # ── User Endpoints ──────────────────────────────────────────────────────────── -@app.post('/user/create') + +@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,) - ) + 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}) - + return json({"user_id": result.response}) -@app.get('/user//balance') +@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' - ) + 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}) + return json({"balance": result.response}) -@app.get('/user//items') +@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' - ) + 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}) + return json({"items": result.response}) -@app.post('/user//add_balance/') +@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),) + operator=user_operator, key=user_id, function="add_balance", params=(int(amount),) ) result: StyxResponse = await future.get() - return json({'success': result.response}) + return json({"success": result.response}) - # ── Item Endpoints ──────────────────────────────────────────────────────────── -@app.post('/item/create') + +@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) - ) + 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}) + return json({"item_id": result.response}) -@app.get('/item//stock') +@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' - ) + 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}) + return json({"stock": result.response}) -@app.post('/item//add_stock/') +@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),) + operator=item_operator, key=item_id, function="update_stock", params=(int(amount),) ) result: StyxResponse = await future.get() - return json({'success': result.response}) + return json({"success": result.response}) -@app.get('/item//price') +@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' - ) + 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}) + return json({"price": result.response}) # ── Coupon Endpoints ────────────────────────────────────────────────────────── -@app.post('/coupon/create') + +@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)) + 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) + operator=coupon_operator, key=code, function="insert", params=(code, discount) ) result: StyxResponse = await future.get() - return json({'coupon_id': result.response}) + return json({"coupon_id": result.response}) -@app.get('/coupon//discount') +@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' - ) + 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}) + return json({"discount": result.response}) # ── Transaction Endpoints ───────────────────────────────────────────────────── -@app.post('/buy_item///') + +@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) + 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}) + 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) + return json({"purchase_successful": False, "error": str(e)}, status=400) -@app.post('/user//transfer//') +@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)) + operator=user_operator, key=user_id, function="transfer_balance", params=(recipient_id, int(amount)) ) result: StyxResponse = await future.get() - return json({'success': result.response}) + return json({"success": result.response}) except NotEnoughBalance as e: - return json({'success': False, 'error': str(e)}, status=400) + return json({"success": False, "error": str(e)}, status=400) -@app.get('/user//is_in_stock/') +@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,) + 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}) + return json({"is_in_stock": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.get('/user//discounted_price//') +@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) + 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}) + return json({"discounted_price": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) - -@app.post('/user//gather_in_loop') + 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', []) + 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) + operator=user_operator, key=user_id, function="gather_in_loop", params=(items, coupons) ) result: StyxResponse = await future.get() - return json({'total': result.response}) + return json({"total": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) - - -@app.post('/user//buy_with_coupon/') + 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 + 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) + 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}) + 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) + return json({"purchase_successful": False, "error": str(e)}, status=400) # ── Bulk / cart endpoints ───────────────────────────────────────────────────── -@app.post('/bulk_purchase_with_tiers/') + +@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', []) + 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) + 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}) + return json({"success": result.response, "latency": result.styx_latency_ms}) except (OutOfStock, NotEnoughBalance) as e: - return json({'success': False, 'error': str(e)}, status=400) + return json({"success": False, "error": str(e)}, status=400) -@app.post('/user//process_cart_with_limits') +@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)) + 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) + 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}) + return json({"purchased": result.response}) except (OutOfStock, NotEnoughBalance) as e: - return json({'purchased': {}, 'error': str(e)}, status=400) + return json({"purchased": {}, "error": str(e)}, status=400) -@app.post('/user//can_afford_cart') +@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', []) + 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,) + operator=user_operator, key=user_id, function="can_afford_cart", params=(items,) ) result: StyxResponse = await future.get() - return json({'can_afford': result.response}) + return json({"can_afford": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.post('/user//multi_restock') +@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', []) + 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) + operator=user_operator, key=user_id, function="multi_restock", params=(items, amounts) ) result: StyxResponse = await future.get() - return json({'total_added': result.response}) + return json({"total_added": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.post('/user//group_items_by_price_bucket') +@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', []) + 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,) + operator=user_operator, key=user_id, function="group_items_by_price_bucket", params=(items,) ) result: StyxResponse = await future.get() - return json({'buckets': result.response}) + return json({"buckets": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.post('/recursion/') +@app.post("/recursion/") async def recursion_test(request, user_id: str): body = request.json or {} - items = body.get('items', []) + items = body.get("items", []) try: future = await styx_client.send_event( - operator=user_operator, - key=user_id, - function='recursion_test', - params=(items,) + operator=user_operator, key=user_id, function="recursion_test", params=(items,) ) result: StyxResponse = await future.get() - return json({'recursion_test': result.response}) + return json({"recursion_test": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) # ── Read-only / utility endpoints ───────────────────────────────────────────── -@app.get('/inventory_value/') + +@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=() + 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}) + return json({"inventory_value": result.response, "latency": result.styx_latency_ms}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.get('/inventory_value_gather/') + +@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=() + 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}) + return json({"inventory_value_gather": result.response, "latency": result.styx_latency_ms}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) + -@app.get('/my_item_prices/') +@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=() - ) + 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}) + return json({"my_item_prices": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -@app.get('/user//most_valuable_item_price') +@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=() + 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}) + return json({"most_valuable_item_price": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) # ── Item bulk create (itemidx removed) ─────────────────────────────────────── @@ -559,20 +487,15 @@ async def most_valuable_item_price(request, user_id: str): # }) -@app.get('/demo/') +@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=() - ) + 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}) + return json({"demo": result.response}) except Exception as e: - return json({'error': str(e)}, status=400) + return json({"error": str(e)}, status=400) -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8002, debug=True) \ No newline at end of file +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8002, debug=True) From 70ebb6bb697da21fca49969116fe574338e648a7 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 13:49:19 +0200 Subject: [PATCH 3/5] ruff fixes --- obol/pyproject.toml | 19 ++++++++++++++----- obol/working-example/app.py | 3 ++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/obol/pyproject.toml b/obol/pyproject.toml index 35df20fe..3706a2dc 100644 --- a/obol/pyproject.toml +++ b/obol/pyproject.toml @@ -72,7 +72,7 @@ port.exclude_lines = [ [tool.ruff] src = ["src"] -exclude = [] +exclude = ["examples"] # compiler inputs / generated output: don't lint or format line-length = 120 # how long you want lines to be [tool.ruff.format] @@ -116,10 +116,19 @@ exclude = [ [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 + "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] diff --git a/obol/working-example/app.py b/obol/working-example/app.py index 20656f1d..23d630fd 100644 --- a/obol/working-example/app.py +++ b/obol/working-example/app.py @@ -27,7 +27,7 @@ app = Sanic("obol-app") STYX_HOST = os.environ.get("STYX_HOST", "localhost") -STYX_PORT = int(os.environ.get("STYX_PORT", 8888)) +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) @@ -49,6 +49,7 @@ async def handle_options(request): "Access-Control-Max-Age": "86400", }, ) + return None @app.on_response From 762778847d89b598144b22331740b8b516f4d84d Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 18 Jun 2026 14:02:52 +0200 Subject: [PATCH 4/5] ruff fixes --- styx-package/styx/common/stateful_function.py | 1 - 1 file changed, 1 deletion(-) diff --git a/styx-package/styx/common/stateful_function.py b/styx-package/styx/common/stateful_function.py index f2338cb9..df654a18 100644 --- a/styx-package/styx/common/stateful_function.py +++ b/styx-package/styx/common/stateful_function.py @@ -237,7 +237,6 @@ def put(self, value: V) -> None: self.__partition, ) - def _func_context_key(self) -> tuple[str, K]: return ("__func_ctx__", self.__key) From af6f2993943a6bbc03ec6d4d5edfe32738bd2aa0 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 22 Jun 2026 14:19:58 +0200 Subject: [PATCH 5/5] Obol documentation --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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