lamindb 1.6.2__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. lamindb/__init__.py +1 -3
  2. lamindb/_finish.py +32 -16
  3. lamindb/base/types.py +6 -4
  4. lamindb/core/_context.py +127 -57
  5. lamindb/core/_mapped_collection.py +1 -1
  6. lamindb/core/_settings.py +44 -4
  7. lamindb/core/_track_environment.py +5 -2
  8. lamindb/core/loaders.py +1 -1
  9. lamindb/core/storage/_anndata_accessor.py +1 -1
  10. lamindb/core/storage/_tiledbsoma.py +14 -8
  11. lamindb/core/storage/_valid_suffixes.py +0 -1
  12. lamindb/core/storage/_zarr.py +1 -1
  13. lamindb/core/storage/objects.py +13 -8
  14. lamindb/core/storage/paths.py +9 -6
  15. lamindb/core/types.py +1 -1
  16. lamindb/curators/_legacy.py +2 -1
  17. lamindb/curators/core.py +106 -105
  18. lamindb/errors.py +9 -0
  19. lamindb/examples/fixtures/__init__.py +0 -0
  20. lamindb/examples/fixtures/sheets.py +224 -0
  21. lamindb/migrations/0103_remove_writelog_migration_state_and_more.py +1 -1
  22. lamindb/migrations/0105_record_unique_name.py +20 -0
  23. lamindb/migrations/0106_transfer_data_migration.py +25 -0
  24. lamindb/migrations/0107_add_schema_to_record.py +68 -0
  25. lamindb/migrations/0108_remove_record_sheet_remove_sheetproject_sheet_and_more.py +30 -0
  26. lamindb/migrations/0109_record_input_of_runs_alter_record_run_and_more.py +123 -0
  27. lamindb/migrations/0110_rename_values_artifacts_record_linked_artifacts.py +17 -0
  28. lamindb/migrations/0111_remove_record__sort_order.py +148 -0
  29. lamindb/migrations/0112_alter_recordartifact_feature_and_more.py +105 -0
  30. lamindb/migrations/0113_lower_case_branch_and_space_names.py +62 -0
  31. lamindb/migrations/0114_alter_run__status_code.py +24 -0
  32. lamindb/migrations/0115_alter_space_uid.py +52 -0
  33. lamindb/migrations/{0104_squashed.py → 0115_squashed.py} +261 -257
  34. lamindb/models/__init__.py +4 -3
  35. lamindb/models/_describe.py +88 -31
  36. lamindb/models/_feature_manager.py +627 -658
  37. lamindb/models/_label_manager.py +1 -3
  38. lamindb/models/artifact.py +214 -99
  39. lamindb/models/collection.py +7 -1
  40. lamindb/models/feature.py +288 -60
  41. lamindb/models/has_parents.py +3 -3
  42. lamindb/models/project.py +32 -15
  43. lamindb/models/query_manager.py +7 -1
  44. lamindb/models/query_set.py +118 -41
  45. lamindb/models/record.py +140 -94
  46. lamindb/models/run.py +42 -42
  47. lamindb/models/save.py +102 -16
  48. lamindb/models/schema.py +41 -8
  49. lamindb/models/sqlrecord.py +105 -40
  50. lamindb/models/storage.py +278 -0
  51. lamindb/models/transform.py +10 -2
  52. lamindb/models/ulabel.py +9 -1
  53. lamindb/py.typed +0 -0
  54. lamindb/setup/__init__.py +2 -1
  55. lamindb/setup/_switch.py +16 -0
  56. lamindb/setup/errors/__init__.py +4 -0
  57. lamindb/setup/types/__init__.py +4 -0
  58. {lamindb-1.6.2.dist-info → lamindb-1.7.0.dist-info}/METADATA +5 -5
  59. {lamindb-1.6.2.dist-info → lamindb-1.7.0.dist-info}/RECORD +61 -44
  60. lamindb/models/core.py +0 -135
  61. {lamindb-1.6.2.dist-info → lamindb-1.7.0.dist-info}/LICENSE +0 -0
  62. {lamindb-1.6.2.dist-info → lamindb-1.7.0.dist-info}/WHEEL +0 -0
lamindb/models/record.py CHANGED
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, overload
4
4
 
5
5
  from django.db import models
6
6
  from django.db.models import CASCADE, PROTECT
7
+ from lamin_utils import logger
7
8
 
8
9
  from lamindb.base.fields import (
9
10
  BooleanField,
@@ -13,21 +14,29 @@ from lamindb.base.fields import (
13
14
  )
14
15
  from lamindb.errors import FieldValidationError
15
16
 
16
- from ..base.ids import base62_12, base62_16
17
+ from ..base.ids import base62_16
17
18
  from .artifact import Artifact
18
19
  from .can_curate import CanCurate
19
20
  from .feature import Feature
21
+ from .has_parents import _query_relatives
22
+ from .query_set import reorder_subset_columns_in_df
20
23
  from .run import Run, TracksRun, TracksUpdates
21
24
  from .sqlrecord import BaseSQLRecord, IsLink, SQLRecord, _get_record_kwargs
25
+ from .transform import Transform
22
26
  from .ulabel import ULabel
23
27
 
24
28
  if TYPE_CHECKING:
25
- from .project import Project
29
+ import pandas as pd
30
+
31
+ from .project import Person, Project, Reference
32
+ from .query_set import QuerySet
26
33
  from .schema import Schema
27
34
 
28
35
 
29
36
  class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
30
- """Flexible records to register, e.g., samples, donors, cells, compounds, sequences.
37
+ """Flexible records as you find them in Excel-like sheets.
38
+
39
+ Useful register, e.g., samples, donors, cells, compounds, sequences.
31
40
 
32
41
  This is currently more convenient to use through the UI.
33
42
 
@@ -39,12 +48,8 @@ class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
39
48
  description: `str` A description.
40
49
 
41
50
  See Also:
42
- :meth:`~lamindb.Sheet`
43
- Sheets to group records.
44
51
  :meth:`~lamindb.Feature`
45
- Dimensions of measurement.
46
- :attr:`~lamindb.Artifact.features`
47
- Feature manager for an artifact.
52
+ Dimensions of measurement (e.g. column of a sheet).
48
53
  """
49
54
 
50
55
  class Meta(SQLRecord.Meta, TracksRun.Meta, TracksUpdates.Meta):
@@ -67,11 +72,20 @@ class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
67
72
  """
68
73
  records: Record
69
74
  """Records of this type (can only be non-empty if `is_type` is `True`)."""
70
- is_type: bool = BooleanField(default=False, db_index=True, null=True)
71
- """Distinguish types from instances of the type.
75
+ is_type: bool = BooleanField(default=False, db_index=True)
76
+ """Indicates if record is a `type`.
72
77
 
73
78
  For example, if a record "Compound" is a `type`, the actual compounds "darerinib", "tramerinib", would be instances of that `type`.
74
79
  """
80
+ schema: Schema | None = ForeignKey(
81
+ "Schema", CASCADE, null=True, related_name="records"
82
+ )
83
+ """A schema to enforce for a type (optional).
84
+
85
+ This is mostly parallel to the `schema` attribute of `Artifact`.
86
+
87
+ If `is_type` is `True`, the schema is used to enforce certain features for each records of this type.
88
+ """
75
89
  # naming convention in analogy with Schema
76
90
  components: Record = models.ManyToManyField(
77
91
  "Record", through="RecordRecord", symmetrical=False, related_name="composites"
@@ -79,26 +93,43 @@ class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
79
93
  """Record-like components of this record."""
80
94
  composites: Record
81
95
  """Record-like composites of this record."""
82
- sheet: Sheet | None = ForeignKey(
83
- "Sheet", CASCADE, null=True, related_name="records"
84
- )
85
- """Group records by sheet."""
86
96
  description: str | None = CharField(null=True)
87
97
  """A description (optional)."""
88
- artifacts: Artifact = models.ManyToManyField(
89
- Artifact, through="RecordArtifact", related_name="records"
98
+ linked_artifacts: Artifact = models.ManyToManyField(
99
+ Artifact, through="RecordArtifact", related_name="linked_in_records"
90
100
  )
91
101
  """Linked artifacts."""
92
- runs: Run = models.ManyToManyField(Run, through="RecordRun", related_name="records")
102
+ artifacts: Artifact = models.ManyToManyField(
103
+ Artifact, through="ArtifactRecord", related_name="records"
104
+ )
105
+ """Annotated artifacts."""
106
+ linked_runs: Run = models.ManyToManyField(
107
+ Run, through="RecordRun", related_name="records"
108
+ )
93
109
  """Linked runs."""
110
+ run: Run | None = ForeignKey(
111
+ Run,
112
+ PROTECT,
113
+ related_name="output_records",
114
+ null=True,
115
+ default=None,
116
+ editable=False,
117
+ )
118
+ """Run that created the record."""
119
+ input_of_runs: Run = models.ManyToManyField(Run, related_name="input_records")
120
+ """Runs that use this record as an input."""
94
121
  ulabels: ULabel = models.ManyToManyField(
95
122
  ULabel,
96
123
  through="RecordULabel",
97
124
  related_name="_records", # in transition period
98
125
  )
99
126
  """Linked runs."""
100
- projects: Project
127
+ linked_projects: Project
101
128
  """Linked projects."""
129
+ linked_references: Reference
130
+ """Linked references."""
131
+ linked_people: Person
132
+ """Linked people."""
102
133
 
103
134
  @overload
104
135
  def __init__(
@@ -128,10 +159,14 @@ class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
128
159
  name: str = kwargs.pop("name", None)
129
160
  type: str | None = kwargs.pop("type", None)
130
161
  is_type: bool = kwargs.pop("is_type", False)
131
- sheet: Sheet = kwargs.pop("sheet", None)
132
162
  description: str | None = kwargs.pop("description", None)
163
+ schema = kwargs.pop("schema", None)
164
+ branch = kwargs.pop("branch", None)
165
+ branch_id = kwargs.pop("branch_id", 1)
166
+ space = kwargs.pop("space", None)
167
+ space_id = kwargs.pop("space_id", 1)
133
168
  _skip_validation = kwargs.pop(
134
- "_skip_validation", True
169
+ "_skip_validation", False
135
170
  ) # should not validate records
136
171
  _aux = kwargs.pop("_aux", None)
137
172
  if len(kwargs) > 0:
@@ -139,90 +174,85 @@ class Record(SQLRecord, CanCurate, TracksRun, TracksUpdates):
139
174
  raise FieldValidationError(
140
175
  f"Only {valid_keywords} are valid keyword arguments"
141
176
  )
177
+ if schema and not is_type:
178
+ logger.important("passing schema, treating as type")
179
+ is_type = True
142
180
  super().__init__(
143
181
  name=name,
144
182
  type=type,
145
183
  is_type=is_type,
146
- sheet=sheet,
147
184
  description=description,
185
+ schema=schema,
186
+ branch=branch,
187
+ branch_id=branch_id,
188
+ space=space,
189
+ space_id=space_id,
148
190
  _skip_validation=_skip_validation,
149
191
  _aux=_aux,
150
192
  )
151
193
 
152
-
153
- class Sheet(SQLRecord, TracksRun, TracksUpdates):
154
- """Sheets to group records."""
155
-
156
- class Meta(SQLRecord.Meta, TracksRun.Meta, TracksUpdates.Meta):
157
- abstract = False
158
-
159
- id: int = models.AutoField(primary_key=True)
160
- uid: str = CharField(
161
- editable=False, unique=True, db_index=True, max_length=12, default=base62_12
162
- )
163
- """A universal random id, valid across DB instances."""
164
- name: str = CharField(db_index=True)
165
- """Name or title of sheet."""
166
- schema: Schema | None = ForeignKey(
167
- "Schema", CASCADE, null=True, related_name="sheets"
168
- )
169
- """A schema to enforce for the sheet (optional)."""
170
- description: str | None = CharField(null=True, db_index=True)
171
- """A description (optional)."""
172
- projects: Project
173
- """Linked projects."""
174
-
175
- @overload
176
- def __init__(
177
- self,
178
- name: str,
179
- schema: Schema | None = None,
180
- description: str | None = None,
181
- ): ...
182
-
183
- @overload
184
- def __init__(
185
- self,
186
- *db_args,
187
- ): ...
188
-
189
- def __init__(
190
- self,
191
- *args,
192
- **kwargs,
193
- ):
194
- if len(args) == len(self._meta.concrete_fields):
195
- super().__init__(*args, **kwargs)
196
- return None
197
- if len(args) > 0:
198
- raise ValueError("Only one non-keyword arg allowed")
199
- name: str = kwargs.pop("name", None)
200
- schema: Schema | None = kwargs.pop("schema", None)
201
- description: str | None = kwargs.pop("description", None)
202
- _skip_validation = kwargs.pop("_skip_validation", True)
203
- _aux = kwargs.pop("_aux", None)
204
- if len(kwargs) > 0:
205
- valid_keywords = ", ".join([val[0] for val in _get_record_kwargs(Record)])
206
- raise FieldValidationError(
207
- f"Only {valid_keywords} are valid keyword arguments"
208
- )
209
- super().__init__(
210
- name=name,
211
- schema=schema,
212
- description=description,
213
- _skip_validation=_skip_validation,
214
- _aux=_aux,
194
+ @property
195
+ def is_form(self) -> bool:
196
+ """Check if record is a form (a record type with a validating schema)."""
197
+ return self.schema is not None and self.is_type
198
+
199
+ def query_children(self) -> QuerySet:
200
+ """Query all children of a record type recursively.
201
+
202
+ While `.records` retrieves the direct children, this method
203
+ retrieves all descendants of a record type.
204
+ """
205
+ return _query_relatives([self], "records", self.__class__) # type: ignore
206
+
207
+ def to_pandas(self) -> pd.DataFrame:
208
+ """Export all children of a record type recursively to a pandas DataFrame."""
209
+ assert self.is_type, "Only types can be exported as dataframes" # noqa: S101
210
+ df = self.query_children().df(features="queryset")
211
+ df.columns.values[0] = "__lamindb_record_uid__"
212
+ df.columns.values[1] = "__lamindb_record_name__"
213
+ if self.schema is not None:
214
+ desired_order = self.schema.members.list("name") # only members is ordered!
215
+ else:
216
+ # sort alphabetically for now
217
+ desired_order = df.columns[2:].tolist()
218
+ desired_order.sort()
219
+ df = reorder_subset_columns_in_df(df, desired_order, position=0) # type: ignore
220
+ return df.sort_index() # order by id for now
221
+
222
+ def to_artifact(self, key: str = None) -> Artifact:
223
+ """Export all children of a record type as a `.csv` artifact."""
224
+ from lamindb.core._context import context
225
+
226
+ assert self.is_type, "Only types can be exported as artifacts" # noqa: S101
227
+ if key is None:
228
+ file_suffix = ".csv"
229
+ key = f"sheet_exports/{self.name}{file_suffix}"
230
+ description = f": {self.description}" if self.description is not None else ""
231
+ format: dict[str, Any] = {"suffix": ".csv"} if key.endswith(".csv") else {}
232
+ format["index"] = False
233
+ transform, _ = Transform.objects.get_or_create(
234
+ key="__lamindb_record_export__", type="function"
215
235
  )
236
+ run = Run(transform, initiated_by_run=context.run).save()
237
+ run.input_records.add(self)
238
+ return Artifact.from_df(
239
+ self.to_pandas(),
240
+ key=key,
241
+ description=f"Export of sheet {self.uid}{description}",
242
+ schema=self.schema,
243
+ format=format,
244
+ run=run,
245
+ ).save()
216
246
 
217
247
 
218
248
  class RecordJson(BaseSQLRecord, IsLink):
219
249
  id: int = models.BigAutoField(primary_key=True)
220
250
  record: Record = ForeignKey(Record, CASCADE, related_name="values_json")
221
- feature: Feature = ForeignKey(Feature, CASCADE, related_name="links_recordjson")
251
+ feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_recordjson")
222
252
  value: Any = JSONField(default=None, db_default=None)
223
253
 
224
254
  class Meta:
225
- unique_together = ("record", "feature")
255
+ unique_together = ("record", "feature") # a list is modeled as a list in json
226
256
 
227
257
 
228
258
  class RecordRecord(SQLRecord, IsLink):
@@ -230,43 +260,59 @@ class RecordRecord(SQLRecord, IsLink):
230
260
  record: Record = ForeignKey(
231
261
  Record, CASCADE, related_name="values_record"
232
262
  ) # composite
233
- feature: Feature = ForeignKey(Feature, CASCADE, related_name="links_recordrecord")
263
+ feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_recordrecord")
234
264
  value: Record = ForeignKey(
235
265
  Record, PROTECT, related_name="links_record"
236
266
  ) # component
237
267
 
238
268
  class Meta:
239
- unique_together = ("record", "feature")
269
+ unique_together = ("record", "feature", "value")
240
270
 
241
271
 
242
272
  class RecordULabel(BaseSQLRecord, IsLink):
243
273
  id: int = models.BigAutoField(primary_key=True)
244
274
  record: Record = ForeignKey(Record, CASCADE, related_name="values_ulabel")
245
- feature: Feature = ForeignKey(Feature, CASCADE, related_name="links_recordulabel")
275
+ feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_recordulabel")
246
276
  value: ULabel = ForeignKey(ULabel, PROTECT, related_name="links_record")
247
277
 
248
278
  class Meta:
249
279
  # allows linking exactly one record to one ulabel per feature, because we likely don't want to have Many
250
- unique_together = ("record", "feature")
280
+ unique_together = ("record", "feature", "value")
251
281
 
252
282
 
253
283
  class RecordRun(BaseSQLRecord, IsLink):
254
284
  id: int = models.BigAutoField(primary_key=True)
255
285
  record: Record = ForeignKey(Record, CASCADE, related_name="values_run")
256
- feature: Feature = ForeignKey(Feature, CASCADE, related_name="links_recordrun")
286
+ feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_recordrun")
257
287
  value: Run = ForeignKey(Run, PROTECT, related_name="links_record")
258
288
 
259
289
  class Meta:
260
290
  # allows linking several records to a single run for the same feature because we'll likely need this
261
- unique_together = ("record", "feature")
291
+ unique_together = ("record", "feature", "value")
262
292
 
263
293
 
264
294
  class RecordArtifact(BaseSQLRecord, IsLink):
265
295
  id: int = models.BigAutoField(primary_key=True)
266
296
  record: Record = ForeignKey(Record, CASCADE, related_name="values_artifact")
267
- feature: Feature = ForeignKey(Feature, CASCADE, related_name="links_recordartifact")
268
- value: Artifact = ForeignKey(Artifact, PROTECT, related_name="links_record")
297
+ feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_recordartifact")
298
+ value: Artifact = ForeignKey(Artifact, PROTECT, related_name="links_in_record")
269
299
 
270
300
  class Meta:
271
301
  # allows linking several records to a single artifact for the same feature because we'll likely need this
272
302
  unique_together = ("record", "feature", "value")
303
+
304
+
305
+ # like ArtifactULabel, for annotation
306
+ class ArtifactRecord(BaseSQLRecord, IsLink):
307
+ id: int = models.BigAutoField(primary_key=True)
308
+ artifact: Artifact = ForeignKey(Artifact, CASCADE, related_name="links_record")
309
+ record: Record = ForeignKey(Record, PROTECT, related_name="links_artifact")
310
+ feature: Feature = ForeignKey(
311
+ Feature, PROTECT, null=True, related_name="links_artifactrecord"
312
+ )
313
+ label_ref_is_name: bool | None = BooleanField(null=True)
314
+ feature_ref_is_name: bool | None = BooleanField(null=True)
315
+
316
+ class Meta:
317
+ # allows linking several records to a single artifact for the same feature because we'll likely need this
318
+ unique_together = ("artifact", "record", "feature")
lamindb/models/run.py CHANGED
@@ -27,11 +27,13 @@ from .sqlrecord import BaseSQLRecord, IsLink, SQLRecord
27
27
  if TYPE_CHECKING:
28
28
  from datetime import datetime
29
29
 
30
+ from ._feature_manager import FeatureManager
30
31
  from .artifact import Artifact
31
32
  from .collection import Collection
32
33
  from .feature import FeatureValue
33
34
  from .project import Project
34
35
  from .query_set import QuerySet
36
+ from .record import Record
35
37
  from .transform import Transform
36
38
  from .ulabel import ULabel
37
39
 
@@ -39,18 +41,6 @@ if TYPE_CHECKING:
39
41
  _TRACKING_READY: bool | None = None
40
42
 
41
43
 
42
- class FeatureManager:
43
- """Feature manager."""
44
-
45
- pass
46
-
47
-
48
- class FeatureManagerRun(FeatureManager):
49
- """Feature manager."""
50
-
51
- pass
52
-
53
-
54
44
  def current_run() -> Run | None:
55
45
  global _TRACKING_READY
56
46
 
@@ -206,12 +196,13 @@ class Run(SQLRecord):
206
196
 
207
197
  Args:
208
198
  transform: `Transform` A :class:`~lamindb.Transform` record.
199
+ initiated_by_run: `Run | None = None` The run that triggers this run.
209
200
  reference: `str | None = None` For instance, an external ID or a download URL.
210
201
  reference_type: `str | None = None` For instance, `redun_id`, `nextflow_id` or `url`.
211
202
 
212
203
  See Also:
213
- :meth:`~lamindb.core.Context.track`
214
- Track global runs & transforms for a notebook or script.
204
+ :func:`~lamindb.track`
205
+ Globally track a script or notebook run.
215
206
 
216
207
  Examples:
217
208
 
@@ -234,26 +225,6 @@ class Run(SQLRecord):
234
225
 
235
226
  _name_field: str = "started_at"
236
227
 
237
- features: FeatureManager = FeatureManagerRun # type: ignore
238
- """Features manager.
239
-
240
- Run parameters are tracked via the `Feature` registry, just like all other variables.
241
-
242
- Guide: :ref:`track-run-parameters`
243
-
244
- Example::
245
-
246
- run.features.add_values({
247
- "learning_rate": 0.01,
248
- "input_dir": "s3://my-bucket/mydataset",
249
- "downsample": True,
250
- "preprocess_params": {
251
- "normalization_type": "cool",
252
- "subset_highlyvariable": True,
253
- },
254
- })
255
- """
256
-
257
228
  id: int = models.BigAutoField(primary_key=True)
258
229
  """Internal id, valid only in one DB instance."""
259
230
  # default uid was changed from base62_20 to base62_16 in 1.6.0
@@ -302,6 +273,10 @@ class Run(SQLRecord):
302
273
  """The collections serving as input for this run."""
303
274
  output_collections: Collection
304
275
  """The collections generated by this run."""
276
+ input_records: Record
277
+ """The collections serving as input for this run."""
278
+ output_records: Record
279
+ """The collections generated by this run."""
305
280
  """Parameter values."""
306
281
  _feature_values: FeatureValue = models.ManyToManyField(
307
282
  "FeatureValue", through="RunFeatureValue", related_name="runs"
@@ -338,14 +313,15 @@ class Run(SQLRecord):
338
313
  """Linked projects."""
339
314
  _is_consecutive: bool | None = BooleanField(null=True)
340
315
  """Indicates whether code was consecutively executed. Is relevant for notebooks."""
341
- _status_code: int = models.SmallIntegerField(default=0, db_index=True)
316
+ _status_code: int = models.SmallIntegerField(default=None, db_index=True, null=True)
342
317
  """Status code of the run.
343
318
 
344
- - 0: scheduled
345
- - 1: started
346
- - 2: errored
347
- - 3: aborted
348
- - 4: completed
319
+ - -3: scheduled
320
+ - -2: re-started
321
+ - -1: started
322
+ - 0: completed
323
+ - 1: errored
324
+ - 2: aborted
349
325
  """
350
326
 
351
327
  @overload
@@ -354,6 +330,7 @@ class Run(SQLRecord):
354
330
  transform: Transform,
355
331
  reference: str | None = None,
356
332
  reference_type: str | None = None,
333
+ initiated_by_run: Run | None = None,
357
334
  ): ...
358
335
 
359
336
  @overload
@@ -367,7 +344,6 @@ class Run(SQLRecord):
367
344
  *args,
368
345
  **kwargs,
369
346
  ):
370
- self.features = FeatureManager(self) # type: ignore
371
347
  if len(args) == len(self._meta.concrete_fields):
372
348
  super().__init__(*args, **kwargs)
373
349
  return None
@@ -402,6 +378,30 @@ class Run(SQLRecord):
402
378
  def params(self) -> FeatureManager:
403
379
  return self.features
404
380
 
381
+ @property
382
+ def features(self) -> FeatureManager:
383
+ """Features manager.
384
+
385
+ Run parameters are tracked via the `Feature` registry, just like all other variables.
386
+
387
+ Guide: :ref:`track-run-parameters`
388
+
389
+ Example::
390
+
391
+ run.features.add_values({
392
+ "learning_rate": 0.01,
393
+ "input_dir": "s3://my-bucket/mydataset",
394
+ "downsample": True,
395
+ "preprocess_params": {
396
+ "normalization_type": "cool",
397
+ "subset_highlyvariable": True,
398
+ },
399
+ })
400
+ """
401
+ from ._feature_manager import FeatureManager
402
+
403
+ return FeatureManager(self)
404
+
405
405
  @classmethod
406
406
  def filter(
407
407
  cls,
@@ -441,7 +441,7 @@ class Run(SQLRecord):
441
441
  keys_normalized, field="name", mute=True
442
442
  )
443
443
  ):
444
- return filter_base(FeatureManagerRun, **expressions)
444
+ return filter_base(Run, **expressions)
445
445
  else:
446
446
  params = ", ".join(sorted(np.array(keys_normalized)[~params_validated]))
447
447
  message = f"feature names: {params}"