Skip to content

Meet PyRel

PyRel is a Python library for building a model of how your business works. The fundamental class in PyRel is the Model class, which represents a semantic model: a map of your business that gives your data meaning so people and AI agents can reason about it the same way.

A PyRel model captures the key concepts, relationships, and data sources that define your business domain. For example, a model of a manufacturing business would include concepts like suppliers, parts, and products:

Relationships like “a supplier supplies a part” and “a part is used in a product” define how the concepts are connected. The model also specifies how data maps to the concepts and relationships.

PyRel can compile the model and deploy it as Snowflake objects that can be consumed by SQL queries, dashboards, applications, automation, and AI agents:

Why does that matter? Business rules often get copied into many queries and scripts, and the copies drift apart. With PyRel, you can bake the logic into your database so that it is centralized, consistent, and automatically available to anyone querying the model.

The starter project answers one question: if Northstar Components can’t deliver, which products are affected? The model explorer below shows the model that answers it. Blocks are concepts, and the labeled paths between them are relationships.

Try these:

  • Select the Supplier block to see its properties and where its data comes from.
  • Select the supplies path to see which two concepts it connects.
  • Expand Preview source data to see the CSV rows behind the model.

Supplier disruption

Three concepts describe the business objects. Two relationships connect them: a Supplier supplies a Part, and a Product requires a Part.

SupplierPartProductsuppliesrequires
SupplierPartProductsuppliesrequires
Supplier

A company that provides parts used by products.

Properties

NameType
supplier_id (identity)String
nameString

Relationships

supplies
Supplier supplies Part
Preview source datasuppliers.csv · 3 Supplier entities

Provides supplier identifiers and names.

data/suppliers.csv

ColumnMaps to
supplier_idIdentity
supplier_namename
supplier_idsupplier_name
S001Northstar Components
S002Harbor Metals
S003Apex Plastics
Part

A component used to assemble or support a product.

Properties

NameType
part_id (identity)String
nameString

Relationships

supplies
Supplier supplies Part
requires
Product requires Part
Preview source dataparts.csv · 4 Part entities

Provides part identifiers and names.

data/parts.csv

ColumnMaps to
part_idIdentity
part_namename
part_idpart_name
P001Control board
P002Temperature sensor
P003Aluminum enclosure
P004Mounting bracket
Product

An item assembled from one or more required parts.

Properties

NameType
product_id (identity)String
nameString

Relationships

requires
Product requires Part
Preview source dataproducts.csv · 3 Product entities

Provides product identifiers and names.

data/products.csv

ColumnMaps to
product_idIdentity
product_namename
product_idproduct_name
PR001Cold-chain tracker
PR002Factory gateway
PR003Smart thermostat
Supplier supplies Part

Connects a supplier to a part that the supplier provides.

Fields

FieldValue
Namesupplies
FromSupplier
ToPart
Preview source datasupplier_parts.csv · 4 supplies facts

Records which supplier provides each part.

data/supplier_parts.csv

ColumnMaps to
supplier_idsupplier
part_idpart
supplier_idpart_id
S001P001
S001P002
S002P003
S003P004
Product requires Part

Connects a product to a part needed by that product.

Fields

FieldValue
Namerequires
FromProduct
ToPart
Preview source dataproduct_parts.csv · 8 requires facts

Records which parts each product needs.

data/product_parts.csv

ColumnMaps to
product_idproduct
part_idpart
product_idpart_id
PR001P001
PR001P002
PR001P003
PR002P001
PR002P003
PR002P004
PR003P002
PR003P004
suppliers.csv

Provides supplier identifiers and names.

Populates

Model item
Supplier

Source mapping

supplier_id
Supplier identity
supplier_name
Supplier.name

Preview: 3 rows

supplier_idsupplier_name
S001Northstar Components
S002Harbor Metals
S003Apex Plastics
parts.csv

Provides part identifiers and names.

Populates

Model item
Part

Source mapping

part_id
Part identity
part_name
Part.name

Preview: 4 rows

part_idpart_name
P001Control board
P002Temperature sensor
P003Aluminum enclosure
P004Mounting bracket
products.csv

Provides product identifiers and names.

Populates

Model item
Product

Source mapping

product_id
Product identity
product_name
Product.name

Preview: 3 rows

product_idproduct_name
PR001Cold-chain tracker
PR002Factory gateway
PR003Smart thermostat
supplier_parts.csv

Records which supplier provides each part.

Populates

Model item
supplies

Source mapping

supplier_id
supplier field
part_id
part field

Preview: 4 rows

supplier_idpart_id
S001P001
S001P002
S002P003
S003P004
product_parts.csv

Records which parts each product needs.

Populates

Model item
requires

Source mapping

product_id
product field
part_id
part field

Preview: 8 rows

product_idpart_id
PR001P001
PR001P002
PR001P003
PR002P001
PR002P003
PR002P004
PR003P002
PR003P004

Open the supplier-disruption folder you used in Install and run PyRel. Each file has one job:

  • Directorysupplier-disruption/
    • Directorydata/ starter facts as CSV files
      • suppliers.csv
      • parts.csv
      • products.csv
      • supplier_parts.csv which supplier supplies which part
      • product_parts.csv which product requires which part
    • Directorymodel/ the semantic model
      • __init__.py makes the model ready to import
      • schema.py what things are
      • source.py where the facts come from
    • find_affected_products.py the query
    • pyproject.toml the Python version and packages the project needs
    • raiconfig.yaml your connection and deployment settings

The model is split into two files on purpose:

  • schema.py describes what things are: the concepts, properties, and relationships.
  • source.py describes where the facts come from: which rows create which things.

This split means you can change where the data comes from without changing what the model means. The raiconfig.yaml file isn’t in the download. You created it in Configure your project.

Open model/schema.py. Here is the whole file:

model/schema.py
from relationalai.semantics import Model, String
model = Model("SupplierDisruption")
Supplier = model.Concept("Supplier", identify_by={"supplier_id": String})
Supplier.name = model.Property(f"{Supplier} has {String:name}")
Part = model.Concept("Part", identify_by={"part_id": String})
Part.name = model.Property(f"{Part} has {String:name}")
Product = model.Concept("Product", identify_by={"product_id": String})
Product.name = model.Property(f"{Product} has {String:name}")
Supplier.supplies = model.Relationship(f"{Supplier} supplies {Part}")
Product.requires = model.Relationship(f"{Product} requires {Part}")

Let’s go through it one piece at a time.

model/schema.py
from relationalai.semantics import Model, String
model = Model("SupplierDisruption")

Model("SupplierDisruption") creates a new, empty model named SupplierDisruption. The code saves it in a variable named model. Everything else in the project adds to this model object. For details, see the Model class reference.

model/schema.py
Supplier = model.Concept("Supplier", identify_by={"supplier_id": String})
Supplier.name = model.Property(f"{Supplier} has {String:name}")
  • model.Concept("Supplier", ...) creates a concept named Supplier.
  • identify_by={"supplier_id": String} tells PyRel that a text value named supplier_id makes each supplier unique. Two rows with the same supplier_id describe the same supplier.
  • model.Property(...) adds a property. The text inside reads like a sentence: “Supplier has name.” {Supplier} is the thing the fact is about, and {String:name} is a text value named name.
  • Supplier.name = ... attaches the property to the concept, so later code can use supplier.name.

Part and Product follow the same pattern, with part_id and product_id as their IDs.

The last two lines of schema.py connect the concepts:

model/schema.py
Supplier.supplies = model.Relationship(f"{Supplier} supplies {Part}")
Product.requires = model.Relationship(f"{Product} requires {Part}")
  • model.Relationship(f"{Supplier} supplies {Part}") creates a relationship between suppliers and parts. The text reads like a sentence and names both ends.
  • Supplier.supplies = ... attaches it to Supplier, so you can write supplier.supplies(part).
  • Product.requires works the same way for products and parts.

One supplier can supply many parts, and one part can be required by many products. These two relationships are the arrows in the concept picture. They let a query move from a supplier to its parts, and then from those parts to products.

Open model/source.py. This file fills the model with facts.

The starter reads small CSV files so it runs anywhere. Real projects usually read Snowflake tables instead. To learn how, see Use a Snowflake table with Model.Table.

Here is the whole file:

model/source.py
from pathlib import Path
from pandas import read_csv
from .schema import Part, Product, Supplier, model
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
supplier_rows = model.data(read_csv(DATA_DIR / "suppliers.csv"))
model.define(
supplier := Supplier.new(supplier_id=supplier_rows["supplier_id"]),
supplier.name(supplier_rows["supplier_name"]),
)
part_rows = model.data(read_csv(DATA_DIR / "parts.csv"))
model.define(
part := Part.new(part_id=part_rows["part_id"]),
part.name(part_rows["part_name"]),
)
product_rows = model.data(read_csv(DATA_DIR / "products.csv"))
model.define(
product := Product.new(product_id=product_rows["product_id"]),
product.name(product_rows["product_name"]),
)
supplier_part_rows = model.data(read_csv(DATA_DIR / "supplier_parts.csv"))
model.define(
Supplier.lookup(supplier_id=supplier_part_rows["supplier_id"]).supplies(
Part.lookup(part_id=supplier_part_rows["part_id"]),
)
)
product_part_rows = model.data(read_csv(DATA_DIR / "product_parts.csv"))
model.define(
Product.lookup(product_id=product_part_rows["product_id"]).requires(
Part.lookup(part_id=product_part_rows["part_id"]),
)
)

Each concept gets its facts from one CSV file. Here is data/suppliers.csv:

data/suppliers.csv
supplier_id,supplier_name
S001,Northstar Components
S002,Harbor Metals
S003,Apex Plastics

And here is the code that reads it:

model/source.py
supplier_rows = model.data(read_csv(DATA_DIR / "suppliers.csv"))
model.define(
supplier := Supplier.new(supplier_id=supplier_rows["supplier_id"]),
supplier.name(supplier_rows["supplier_name"]),
)
  • read_csv(...) reads the file into a table of rows.
  • model.data(...) gives those rows to the model.
  • Supplier.new(supplier_id=...) creates one supplier for each row, using the supplier_id column as its ID. The := saves the new supplier in a variable named supplier, so the next line can use it.
  • supplier.name(...) sets each supplier’s name from the supplier_name column.
  • model.define(...) adds these facts to the model.

So the row S001,Northstar Components becomes one supplier with the ID S001 and the name Northstar Components. The Part and Product blocks work the same way.

Relationship files hold pairs of IDs. Here is data/supplier_parts.csv:

data/supplier_parts.csv
supplier_id,part_id
S001,P001
S001,P002
S002,P003
S003,P004

And here is the code that reads it:

model/source.py
supplier_part_rows = model.data(read_csv(DATA_DIR / "supplier_parts.csv"))
model.define(
Supplier.lookup(supplier_id=supplier_part_rows["supplier_id"]).supplies(
Part.lookup(part_id=supplier_part_rows["part_id"]),
)
)
  • Supplier.lookup(supplier_id=...) finds the supplier that already has that ID.
  • Part.lookup(part_id=...) finds the part with that ID.
  • .supplies(...) connects the two.

So the row S001,P001 means “Northstar Components supplies the Control board.” The product_parts.csv block connects products to parts with Product.requires in the same way.

The last file in the folder is model/__init__.py:

model/__init__.py
from .schema import Part, Product, Supplier, model
from . import source as source
__all__ = ["Part", "Product", "Supplier", "model"]

The line from . import source runs source.py whenever another file imports model. That’s how the query script gets a model that already has all its facts.

You deployed the starter model once in Install and run PyRel. This time, you’ll deploy it with a dev profile. A profile is a named group of settings in raiconfig.yaml. With a dev profile, you can later add a prod profile for the same model and switch between them.

  1. Open raiconfig.yaml and find the deployment section. It looks like this, with your own schema names:

    raiconfig.yaml (before)
    deployment:
    public_schema: MY_DATABASE.MY_SCHEMA
    identity:
    internal_schema: MY_DATABASE.MY_SCHEMA_META
    schedules:
    on_deploy:
    interval_s: 0
    run_on_deploy: true
    schedule: on_deploy
  2. Move the whole deployment section under profile.dev, and add active_profile: dev below it. Indent the deployment section two more levels. Leave your connections section and other settings as they are.

    raiconfig.yaml (after)
    profile:
    dev:
    deployment:
    public_schema: MY_DATABASE.MY_SCHEMA
    identity:
    internal_schema: MY_DATABASE.MY_SCHEMA_META
    schedules:
    on_deploy:
    interval_s: 0
    run_on_deploy: true
    schedule: on_deploy
    active_profile: dev

    active_profile: dev tells PyRel to use the dev settings.

  3. In the supplier-disruption folder, deploy the model:

    uv
    Terminal window
    uv run rai models deploy --path model
    pip
    Terminal window
    rai models deploy --path model

    --path model tells the command where the model code is. The command creates the deployed objects, fills them with data, and waits until that first data load finishes. This can take a minute or two.

    If the deploy fails with an EXECUTE TASK error, ask your administrator to run GRANT EXECUTE TASK ON ACCOUNT TO ROLE RAI_DEVELOPER;. Then deploy again.

  4. In Snowsight, open your deployment schema to see what the deploy created:

    • Views named SUPPLIER, PART, and PRODUCT. Each concept becomes a view with a column for its ID and a column for its name.
    • Tables named SUPPLIER_SUPPLIES and PRODUCT_REQUIRES. Each relationship becomes a table. Each row holds the IDs of two connected things, such as a supplier ID and a part ID.

    The deploy filled the tables once. They don’t change until you deploy again. The metadata schema holds the deployment’s own records, so you don’t need to open it.

A prod profile works the same way, with a different schema. To learn more, see Dev, Staging, and Production environments and Deploy a model.

Now ask the deployed model the starter question: which products need a part from Northstar Components? Choose a tab. Both tabs get the same answer from the same deployed model.

Run the query script:

uv
Terminal window
uv run python find_affected_products.py
pip
Terminal window
python find_affected_products.py
Output
product part
0 Cold-chain tracker Control board
1 Cold-chain tracker Temperature sensor
2 Factory gateway Control board
3 Smart thermostat Temperature sensor

Now that the model is deployed, the script gets its answers from the views and tables in your dev schema. Here is the script:

find_affected_products.py
from model import Part, Product, Supplier, model
unavailable_supplier = Supplier.ref()
affected_part = Part.ref()
affected_product = Product.ref()
(
model.where(
unavailable_supplier.name == "Northstar Components",
unavailable_supplier.supplies(affected_part),
affected_product.requires(affected_part),
)
.select(
affected_product.name.alias("product"),
affected_part.name.alias("part"),
)
.inspect()
)
  • Supplier.ref(), Part.ref(), and Product.ref() create placeholders. Each one stands for “some supplier,” “some part,” or “some product.”
  • model.where(...) lists the rules the placeholders must follow: the supplier’s name is Northstar Components, the supplier supplies the part, and the product requires the part.
  • .select(...) picks what to return: the product name and the part name.
  • .alias("product") and .alias("part") name the output columns.
  • .inspect() runs the query and prints the results.
  • Each product that depends on a part supplied by Northstar Components.
  • The part supplied by Northstar that each product depends on.
  • Whether production will stop.
  • Which supplier should replace Northstar.
  • Answers to those questions, which RelationalAI can provide with a richer model that includes current inventory, lead times, and alternative suppliers, together with the appropriate reasoning.

Deployed objects take up storage and clutter your schema. When you’re done, remove them.

  1. Preview what will be removed:

    uv
    Terminal window
    uv run rai models teardown
    pip
    Terminal window
    rai models teardown

    This only prints a list. Nothing is removed yet.

  2. Remove the deployed objects:

    uv
    Terminal window
    uv run rai models teardown --force
    pip
    Terminal window
    rai models teardown --force

    This removes the views, tables, and other objects the deploy created. Your schemas and your raiconfig.yaml file stay, so you can deploy again at any time.

Choose a guide based on what you want to learn next: