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
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]
)