Related records are rows that belong together across tables. Here every customer order has 1 to 5 line items with random products, prices and quantities, and a total computed from them. The orders and their items are written to two PostgreSQL tables, orders and order_items. Each tab shows the example written one way. In YAML a mapping payload is a constant and cannot hold random values, so the order generator is a Python process in postgres_orders_logic.py, referenced with !python. Call Python from YAML explains how references work. The connection string names odctl, the database that the odctl postgres profile creates, with the user user and the password password.
Look for each order's line items in order_items, with the same order_id as the order's row in orders, and for a total_amount that is the sum of its items.
What It Does
When the run starts, each PostgresEgress creates its table, orders or order_items, and PostgresIngress creates simulation_params, if they do not exist. The run then writes orders and their items until you stop it. Both egresses receive every record. Each keeps only the records whose __table__ key names its table.
Before You Start
The example needs Docker and the odctl profile postgres, which each Quick Start below starts and stops. Services for the Examples shows how to install odctl.
Steps
Run It
The generator is listed under processes with kwargs, so it is called as order_generator(context, arrival="orders", max_items=5). It draws from context.sampler.rng, the random number generator seeded by random_seed, so a seeded run repeats the same orders.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
# 1. Installuvtoolinstall"desflow[postgres]"--with-executables-from"odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apiyaml
cdrelated-records/yaml
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)desflowrunpostgres_orders.yaml
# 5. Stop its service when finishedodctldownpostgres--volumes
# 1. Installpipinstall"desflow[postgres]""odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apiyaml
cdrelated-records/yaml
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)desflowrunpostgres_orders.yaml
# 5. Stop its service when finishedodctldownpostgres--volumes
The generator is added with add_process, so it is called as order_generator(context, arrival="orders", max_items=5). It draws from context.sampler.rng, the random number generator seeded by random_seed, so a seeded run repeats the same orders.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
# 1. Installuvtoolinstall"desflow[postgres]"--with-executables-from"odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apibuilder
cdrelated-records/builder
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)uvrun--no-project--with"desflow[postgres]"postgres_orders_example.py
# 5. Stop its service when finishedodctldownpostgres--volumes
# 1. Installpipinstall"desflow[postgres]""odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apibuilder
cdrelated-records/builder
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)pythonpostgres_orders_example.py
# 5. Stop its service when finishedodctldownpostgres--volumes
The generator is a SimPy process started with env.process. It draws the gaps between orders and the order contents from one Sampler seeded with 42, in the same order as the other two versions, so it publishes the same orders.
Quick Start
desflow examples get downloads the example at the release you install, into a folder of its own. Pick uv or pip:
# 1. Installuvtoolinstall"desflow[postgres]"--with-executables-from"odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apilow-level
cdrelated-records/low-level
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)uvrun--no-project--with"desflow[postgres]"postgres_orders_example.py
# 5. Stop its service when finishedodctldownpostgres--volumes
# 1. Installpipinstall"desflow[postgres]""odctl>=1.0,<2"# 2. Download the exampledesflowexamplesgetrelated-records--apilow-level
cdrelated-records/low-level
# 3. Start its serviceodctluppostgres
# 4. Run it (Ctrl + C to stop)pythonpostgres_orders_example.py
# 5. Stop its service when finishedodctldownpostgres--volumes
Change a Parameter Live
While the run goes, raise the arrival rate from a second terminal. The command is the same for every form:
# Change the order arrival rate of the running simulationdockerexec-itpostgrespsql-Uuser-dodctl-c"INSERT INTO simulation_params (path, value) VALUES ('Shop.arrival.orders.rate', '5.0');"
Check the Result
The three tabs write the same records, which a test checks by running each version unpaced. A real run writes each order as a row of orders and each line item as a row of order_items. desflow run postgres_orders.yaml --sample 2 --stdout prints the first two records as JSON lines instead, with no database. They are the one line item of orders-1, then the order itself:
This command leaves out the __table__ key that picks each record's table, because only a PostgreSQL sink uses it. It logs a warning that says so. The order's total_amount is its item's quantity times its unit_price.
Full Files
Full files
Files live in the examples/yaml/ folder of the repository, and the label on each block below is its path there.
# Orders with random line items, in YAML with one Python function.## Each order arrival builds an order with 1 to 5 items, random prices and# quantities, and a total computed from them. A mapping payload is a constant and# cannot hold random values, so the generator is Python in postgres_orders_logic.py# beside this file, referenced with !python. Everything else is plain YAML.## Two PostgresEgress instances are attached, one per table, and each keeps only the# records whose __table__ key names its table. Each creates its table at start.## Needs a database: odctl up postgres. Runs until interrupted with Ctrl + C.# Run it from its folder with: desflow run postgres_orders.yamlsimulation:sim_id:Shopfactor:1.0random_seed:42ingress:-type:Postgresconfig:connection_dsn:postgresql://user:password@127.0.0.1:5432/odctltable_name:simulation_paramsegress:-type:Postgresconfig:connection_dsn:postgresql://user:password@127.0.0.1:5432/odctltables:orders:columns:order_id:INTcustomer_id:INTtotal_amount:REALstatus:TEXTtimestamp:TIMESTAMPprimary_key:order_id-type:Postgresconfig:connection_dsn:postgresql://user:password@127.0.0.1:5432/odctltables:order_items:columns:order_item_id:INTorder_id:INTproduct_id:INTquantity:INTunit_price:REALprimary_key:order_item_idarrivals:# No spawn: the process below waits on this arrival itself.orders:{dist:exponential, rate:1.0}processes:# Called as order_generator(context, arrival="orders", max_items=5).-function:!pythonpostgres_orders_logic.order_generatorkwargs:{arrival:orders, max_items:5}
"""Python for postgres_orders.yaml: an order generator with random line items."""deforder_generator(context,arrival:str,max_items:int):"""Publishes an order and its items on every arrival. Draws from `context.sampler.rng`, the random number generator seeded by `random_seed`, so a seeded run repeats the same orders. The `__table__` key names the table each record is written to. """rng=context.sampler.rngorder_id=1item_id=1whileTrue:# The order's key, such as orders-1, is the record key of all its recordsentity=yieldcontext.wait_for_arrival(arrival)total=0.0for_inrange(int(rng.integers(1,max_items+1))):price=round(float(rng.uniform(10.0,50.0)),2)quantity=int(rng.integers(1,4))total+=price*quantitycontext.env.publish_event(entity.key,{"__table__":"order_items","order_item_id":item_id,"order_id":order_id,"product_id":int(rng.integers(1,51)),"quantity":quantity,"unit_price":price,},)item_id+=1context.env.publish_event(entity.key,{"__table__":"orders","order_id":order_id,"customer_id":int(rng.integers(1,101)),"total_amount":round(total,2),"status":"pending",},)order_id+=1
"""Orders with line items, builder API.The builder version of `yaml/advanced/postgres_orders.yaml`. Each order arrivalbuilds an order with 1 to 5 items, random prices and quantities, and a total computedfrom them. Two PostgresEgress instances are attached, one per table, and each keepsonly the records whose `__table__` key names its table. Each creates its table atstart.Needs a database: odctl up postgres. Runs until interrupted with Ctrl + C."""importloggingfromdesflowimportPostgresEgress,PostgresIngress,SimulationContextlogging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")DSN="postgresql://user:password@127.0.0.1:5432/odctl"ORDERS={"columns":{"order_id":"INT","customer_id":"INT","total_amount":"REAL","status":"TEXT","timestamp":"TIMESTAMP",},"primary_key":"order_id",}ORDER_ITEMS={"columns":{"order_item_id":"INT","order_id":"INT","product_id":"INT","quantity":"INT","unit_price":"REAL",},"primary_key":"order_item_id",}deforder_generator(context:SimulationContext,arrival:str,max_items:int):"""Publishes an order and its items on every arrival. Draws from `context.sampler.rng`, the random number generator seeded by `random_seed`, so a seeded run repeats the same orders. The `__table__` key names the table each record is written to. """rng=context.sampler.rngorder_id=1item_id=1whileTrue:# The order's key, such as orders-1, is the record key of all its recordsentity=yieldcontext.wait_for_arrival(arrival)total=0.0for_inrange(int(rng.integers(1,max_items+1))):price=round(float(rng.uniform(10.0,50.0)),2)quantity=int(rng.integers(1,4))total+=price*quantitycontext.env.publish_event(entity.key,{"__table__":"order_items","order_item_id":item_id,"order_id":order_id,"product_id":int(rng.integers(1,51)),"quantity":quantity,"unit_price":price,},)item_id+=1context.env.publish_event(entity.key,{"__table__":"orders","order_id":order_id,"customer_id":int(rng.integers(1,101)),"total_amount":round(total,2),"status":"pending",},)order_id+=1app=(SimulationContext(sim_id="Shop",factor=1.0,random_seed=42).add_ingress(PostgresIngress(DSN,table_name="simulation_params")).add_egress(PostgresEgress(DSN,tables={"orders":ORDERS})).add_egress(PostgresEgress(DSN,tables={"order_items":ORDER_ITEMS})).add_arrival("orders",dist="exponential",rate=1.0).add_process(order_generator,arrival="orders",max_items=5))if__name__=="__main__":app.run()
"""Orders with line items, low-level API.The low-level version of `builder/postgres_orders_example.py` and`yaml/advanced/postgres_orders.yaml`. It wires `DynamicRealtimeEnvironment`, theregistry and the connectors by hand. With the same seed it publishes the same ordersas the other two.Needs a database: odctl up postgres. Runs until interrupted with Ctrl + C."""importloggingimportnumpyasnpfromdesflowimport(DistributionConfig,DynamicRealtimeEnvironment,PostgresEgress,PostgresIngress,Sampler,SimParameter,)logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")DSN="postgresql://user:password@127.0.0.1:5432/odctl"PARAMS=SimParameter(sim_id="Shop",arrival={"orders":DistributionConfig(dist="exponential",rate=1.0)},)ORDERS={"columns":{"order_id":"INT","customer_id":"INT","total_amount":"REAL","status":"TEXT","timestamp":"TIMESTAMP",},"primary_key":"order_id",}ORDER_ITEMS={"columns":{"order_item_id":"INT","order_id":"INT","product_id":"INT","quantity":"INT","unit_price":"REAL",},"primary_key":"order_item_id",}deforder_generator(env,sampler:Sampler,max_items:int):"""Publishes an order and its items on every arrival. The gaps come from the arrival's own random sequence and the order contents from `sampler.rng`, as in the builder version, so the two publish the same records. """rng=sampler.rngarrival=env.registry.get_config("Shop.arrival.orders")order_id=1item_id=1whileTrue:yieldenv.timeout(sampler.sample(arrival))total=0.0for_inrange(int(rng.integers(1,max_items+1))):price=round(float(rng.uniform(10.0,50.0)),2)quantity=int(rng.integers(1,4))total+=price*quantityenv.publish_event(f"orders-{order_id}",{"__table__":"order_items","order_item_id":item_id,"order_id":order_id,"product_id":int(rng.integers(1,51)),"quantity":quantity,"unit_price":price,},)item_id+=1env.publish_event(f"orders-{order_id}",{"__table__":"orders","order_id":order_id,"customer_id":int(rng.integers(1,101)),"total_amount":round(total,2),"status":"pending",},)order_id+=1defrun(until=None,factor=1.0):env=DynamicRealtimeEnvironment(factor=factor,strict=False)env.registry.register_sim_parameter(PARAMS)env.setup_ingress([PostgresIngress(DSN,table_name="simulation_params")])env.setup_egress([PostgresEgress(DSN,tables={"orders":ORDERS}),PostgresEgress(DSN,tables={"order_items":ORDER_ITEMS}),])sampler=Sampler(rng=np.random.default_rng(42))env.process(order_generator(env,sampler,max_items=5))try:env.run(until=until)finally:env.teardown()if__name__=="__main__":run()