Features
Define one-to-one relationships between feature classes.
Has-one relationships link a feature to a single instance of another feature.
The simplest way to specify a join for a has-one relationship is implicitly. In the example below,
a User
is linked to their Profile
.
from chalk.features import features
@features
class Profile:
id: str
user_id: "User.id"
email_age_years: float
@features
class User:
id: str
profile: Profile
With a has-one relationship established, you can reference features on Profile
through
User
. For example:
user_email_age = User.profile.email_age_years
In the following snippet, the has-one join is explicitly defined. This is functionally equivalent to the recommended implementation:
from chalk.features import features, has_one, ...
@features
class Profile:
id: str
user_id: str
email_age_years: float
@features
class User:
id: str
uid: str
profile: Profile = has_one(lambda: Profile.user_id == User.uid)
The lambda
solves forward references, letting you reference User
before it is defined.
You can also specify a composite join key for a has-one relationship. For example, if a User
is linked to a
Profile
by org
and email
, you can define the join as follows:
from chalk.features import features, has_one
from datetime import datetime
@features
class User:
id: str
email: str = _.alias + "-" + _.org + _.domain
org_domain: str = _.org + _.domain
org: str
domain: str
alias: str
# join with composite key
posts: DataFrame[Posts] = has_many(lambda: User.email == Post.email)
# multi-feature join
org_profile: Profile = has_one(lambda: (User.alias == Profile.email) & (User.org == Profile.org))
@features
class Workspace:
id: str
# join with child-class's composite key
users: DataFrame[Users] = has_many(lambda: Workspace.id == User.org_domain)
You can also add a back-reference to User
from Profile
.
However, you don’t have to explicitly set the join on Profile
.
Instead, the join condition is assumed to be symmetric and copied over.
To complete the one-to-one relationship from our example, add a User
to the Profile
class:
@features
class Profile:
...
user_id: "User.uid"
email_age_years: float
user: "User"
@features
class User:
...
uid: str
profile: Profile
Here you need to use quotes around `User` to use a forward reference.
When a has-one relationship is specified, the default behavior is to treat the linked Feature
as required. Following the example above, specifying a User
without a Profile
and
querying for a User
’s profile or using the User.profile
in a resolver raises an
error.
To define optional relationships, use the typing.Optional[...]
keyword:
from typing import Optional
@features
class User:
...
uid: Profile.user_id
profile: Profile
profile: Optional[Profile]
Note, resolvers that take optional features as inputs need to handle the None
case. This is
covered in more detail in the resolver’s section of the docs.
You can chain has-one joins to traverse multiple relationships. For example, you could define the following features to represent a user’s profile and preferences in an application.
from chalk.features import features, Primary
@features
class User:
id: str
email: str
@features
class Profile:
id: Primary[User.id]
username: str
@features
class Preferences:
id: Primary[Profile.id]
dark_mode: bool
In larger projects, it’s common to split feature definitions across multiple Python modules.
For unidirectional dependencies, this is straightforward. For example, if user.py
imports
profile.py
, you can define the User
and Profile
features in separate files without
issues. However, if you have circular dependencies, you may run into problems.
Chalk supports this, but circular imports can arise when features reference each other across files.
To avoid these issues, use the if TYPE_CHECKING
block from the typing
module and quote your forward references.
Here’s an example of how to do this cleanly:
# Imports User directly, because user.py
# wont import profile.py
from src.user import User
@features
class Profile:
id: Primary[User.id]
username: str
from chalk.features import features
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Imports Profile only when type checking
# to avoid circular imports
from src.profile import Profile
@features
class User:
id: str
# Profile must be quoted because it is imported
# only when type checking
profile: "Profile"
By quoting imports inside if TYPE_CHECKING
,
you avoid circular dependency errors while still
maintaining type safety and feature linkage.
You can also query for a feature that is joined through a has-one relationship by
referencing the root namespace. For example, to query for the Profile
features
associated with a User
continuing from the example above, you can write:
from chalk.client import ChalkClient
from src.features import User
client = ChalkClient()
client.query(
input={User.id: "1"},
output=[User.profile.id, User.profile.email_age_years]
)