liminal-orm 1.1.4__py3-none-any.whl → 2.0.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 (33) hide show
  1. liminal/base/base_operation.py +4 -3
  2. liminal/base/base_validation_filters.py +15 -0
  3. liminal/base/name_template_parts.py +96 -0
  4. liminal/base/properties/base_field_properties.py +2 -2
  5. liminal/base/properties/base_name_template.py +83 -0
  6. liminal/base/properties/base_schema_properties.py +13 -1
  7. liminal/dropdowns/compare.py +8 -0
  8. liminal/dropdowns/operations.py +1 -1
  9. liminal/entity_schemas/api.py +18 -0
  10. liminal/entity_schemas/compare.py +62 -8
  11. liminal/entity_schemas/entity_schema_models.py +43 -0
  12. liminal/entity_schemas/generate_files.py +13 -11
  13. liminal/entity_schemas/operations.py +43 -18
  14. liminal/entity_schemas/tag_schema_models.py +146 -3
  15. liminal/entity_schemas/utils.py +15 -2
  16. liminal/enums/__init__.py +0 -1
  17. liminal/enums/benchling_entity_type.py +8 -0
  18. liminal/enums/name_template_part_type.py +12 -0
  19. liminal/external/__init__.py +11 -1
  20. liminal/migrate/revisions_timeline.py +2 -1
  21. liminal/orm/base_model.py +90 -29
  22. liminal/orm/name_template.py +39 -0
  23. liminal/orm/schema_properties.py +27 -1
  24. liminal/tests/conftest.py +18 -9
  25. liminal/tests/test_entity_schema_compare.py +61 -12
  26. liminal/utils.py +9 -0
  27. liminal/validation/__init__.py +84 -108
  28. liminal/{enums/benchling_report_level.py → validation/validation_severity.py} +2 -2
  29. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.0.dist-info}/METADATA +17 -20
  30. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.0.dist-info}/RECORD +33 -29
  31. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.0.dist-info}/LICENSE.md +0 -0
  32. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.0.dist-info}/WHEEL +0 -0
  33. {liminal_orm-1.1.4.dist-info → liminal_orm-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -2,12 +2,14 @@ from typing import Any, ClassVar
2
2
 
3
3
  from liminal.base.base_operation import BaseOperation
4
4
  from liminal.base.properties.base_field_properties import BaseFieldProperties
5
+ from liminal.base.properties.base_name_template import BaseNameTemplate
5
6
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
6
7
  from liminal.connection import BenchlingService
7
8
  from liminal.dropdowns.utils import get_benchling_dropdown_id_name_map
8
9
  from liminal.entity_schemas.api import (
9
10
  archive_tag_schemas,
10
11
  create_entity_schema,
12
+ set_tag_schema_name_template,
11
13
  unarchive_tag_schemas,
12
14
  update_tag_schema,
13
15
  )
@@ -21,7 +23,7 @@ from liminal.entity_schemas.utils import (
21
23
  convert_tag_schema_field_to_field_properties,
22
24
  convert_tag_schema_to_internal_schema,
23
25
  )
24
- from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
26
+ from liminal.enums import BenchlingNamingStrategy
25
27
  from liminal.orm.schema_properties import SchemaProperties
26
28
  from liminal.utils import to_snake_case
27
29
 
@@ -121,7 +123,7 @@ class CreateEntitySchema(BaseOperation):
121
123
  f"Entity schema {self._validated_schema_properties.warehouse_name} is already active in Benchling."
122
124
  )
123
125
  dropdowns_map = get_benchling_dropdown_id_name_map(benchling_service)
124
- benchling_schema_props, benchling_fields_props = (
126
+ benchling_schema_props, _, benchling_fields_props = (
125
127
  convert_tag_schema_to_internal_schema(schema, dropdowns_map)
126
128
  )
127
129
  if (
@@ -135,7 +137,7 @@ class CreateEntitySchema(BaseOperation):
135
137
 
136
138
 
137
139
  class ArchiveEntitySchema(BaseOperation):
138
- order: ClassVar[int] = 180
140
+ order: ClassVar[int] = 170
139
141
 
140
142
  def __init__(self, wh_schema_name: str) -> None:
141
143
  self.wh_schema_name = wh_schema_name
@@ -160,7 +162,7 @@ class ArchiveEntitySchema(BaseOperation):
160
162
 
161
163
 
162
164
  class UnarchiveEntitySchema(BaseOperation):
163
- order: ClassVar[int] = 110
165
+ order: ClassVar[int] = 100
164
166
 
165
167
  def __init__(self, wh_schema_name: str) -> None:
166
168
  self.wh_schema_name = wh_schema_name
@@ -245,20 +247,43 @@ class UpdateEntitySchema(BaseOperation):
245
247
  raise ValueError(
246
248
  f"Entity schema prefix {self.update_props.prefix} already exists in Benchling."
247
249
  )
248
- if (
249
- self.update_props.naming_strategies
250
- and not BenchlingNamingStrategy.is_valid_set(
251
- self.update_props.naming_strategies, bool(tag_schema.nameTemplateParts)
252
- )
253
- ):
254
- raise ValueError(
255
- "Invalid naming strategies for schema. The name template must be set on the schema through the UI when using template-based naming strategies."
256
- )
257
250
  return tag_schema
258
251
 
259
252
 
253
+ class UpdateEntitySchemaNameTemplate(BaseOperation):
254
+ order: ClassVar[int] = 150
255
+
256
+ def __init__(
257
+ self,
258
+ wh_schema_name: str,
259
+ update_name_template: BaseNameTemplate,
260
+ ) -> None:
261
+ self.wh_schema_name = wh_schema_name
262
+ self.update_name_template = update_name_template
263
+
264
+ def execute(self, benchling_service: BenchlingService) -> dict[str, Any]:
265
+ tag_schema = TagSchemaModel.get_one(benchling_service, self.wh_schema_name)
266
+ updated_schema = tag_schema.update_name_template(self.update_name_template)
267
+ return set_tag_schema_name_template(
268
+ benchling_service,
269
+ tag_schema.id,
270
+ {
271
+ "nameTemplateParts": [
272
+ part.model_dump() for part in updated_schema.nameTemplateParts
273
+ ],
274
+ "shouldOrderNamePartsBySequence": updated_schema.shouldOrderNamePartsBySequence,
275
+ },
276
+ )
277
+
278
+ def describe_operation(self) -> str:
279
+ return f"{self.wh_schema_name}: Updating name template to {str(self.update_name_template)}."
280
+
281
+ def describe(self) -> str:
282
+ return f"{self.wh_schema_name}: Name template is different in code versus Benchling."
283
+
284
+
260
285
  class CreateEntitySchemaField(BaseOperation):
261
- order: ClassVar[int] = 120
286
+ order: ClassVar[int] = 110
262
287
 
263
288
  def __init__(
264
289
  self,
@@ -345,7 +370,7 @@ class CreateEntitySchemaField(BaseOperation):
345
370
 
346
371
 
347
372
  class ArchiveEntitySchemaField(BaseOperation):
348
- order: ClassVar[int] = 160
373
+ order: ClassVar[int] = 140
349
374
 
350
375
  def __init__(
351
376
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -396,7 +421,7 @@ class ArchiveEntitySchemaField(BaseOperation):
396
421
 
397
422
 
398
423
  class UnarchiveEntitySchemaField(BaseOperation):
399
- order: ClassVar[int] = 130
424
+ order: ClassVar[int] = 120
400
425
 
401
426
  def __init__(
402
427
  self, wh_schema_name: str, wh_field_name: str, index: int | None = None
@@ -443,7 +468,7 @@ class UnarchiveEntitySchemaField(BaseOperation):
443
468
 
444
469
 
445
470
  class UpdateEntitySchemaField(BaseOperation):
446
- order: ClassVar[int] = 140
471
+ order: ClassVar[int] = 130
447
472
 
448
473
  def __init__(
449
474
  self,
@@ -514,7 +539,7 @@ class UpdateEntitySchemaField(BaseOperation):
514
539
 
515
540
 
516
541
  class ReorderEntitySchemaFields(BaseOperation):
517
- order: ClassVar[int] = 170
542
+ order: ClassVar[int] = 160
518
543
 
519
544
  def __init__(self, wh_schema_name: str, new_order: list[str]) -> None:
520
545
  self.wh_schema_name = wh_schema_name
@@ -5,7 +5,11 @@ from typing import Any
5
5
  import requests
6
6
  from pydantic import BaseModel
7
7
 
8
+ from liminal.base.name_template_parts import (
9
+ NameTemplatePart,
10
+ )
8
11
  from liminal.base.properties.base_field_properties import BaseFieldProperties
12
+ from liminal.base.properties.base_name_template import BaseNameTemplate
9
13
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
10
14
  from liminal.connection import BenchlingService
11
15
  from liminal.dropdowns.utils import get_benchling_dropdown_summary_by_name
@@ -15,6 +19,7 @@ from liminal.enums import (
15
19
  BenchlingFolderItemType,
16
20
  BenchlingSequenceType,
17
21
  )
22
+ from liminal.enums.name_template_part_type import NameTemplatePartType
18
23
  from liminal.mappers import (
19
24
  convert_entity_type_to_api_entity_type,
20
25
  convert_field_type_to_api_field_type,
@@ -32,6 +37,73 @@ class FieldRequiredLinkShortModel(BaseModel):
32
37
  storableSchema: dict[str, Any] | None = None
33
38
 
34
39
 
40
+ class NameTemplatePartModel(BaseModel):
41
+ """A pydantic model to define a part of a name template."""
42
+
43
+ type: NameTemplatePartType
44
+ fieldId: str | None = None
45
+ text: str | None = None
46
+ datetimeFormat: str | None = None
47
+
48
+ @classmethod
49
+ def from_name_template_part(
50
+ cls, part: NameTemplatePart, fields: list[TagSchemaFieldModel] | None = None
51
+ ) -> NameTemplatePartModel:
52
+ data = part.model_dump()
53
+ field_id = None
54
+ if wh_field_name := data.get("wh_field_name"):
55
+ field = next((f for f in fields if f.systemName == wh_field_name), None)
56
+ if field is None:
57
+ raise ValueError(f"Field {wh_field_name} not found in fields")
58
+ field_id = field.id
59
+ if part.component_type == NameTemplatePartType.CHILD_ENTITY_LOT_NUMBER:
60
+ if not field.isParentLink:
61
+ raise ValueError(
62
+ f"Field {wh_field_name} is not a parent link field. The field for type {part.component_type} must be a parent link field."
63
+ )
64
+ return cls(
65
+ type=part.component_type,
66
+ fieldId=field_id,
67
+ text=data.get("value"),
68
+ )
69
+
70
+ def to_name_template_part(
71
+ self, fields: list[TagSchemaFieldModel] | None = None
72
+ ) -> NameTemplatePart:
73
+ part_cls = NameTemplatePart.resolve_type(self.type)
74
+ if self.fieldId:
75
+ field = next((f for f in fields if f.apiId == self.fieldId), None)
76
+ if field is None:
77
+ raise ValueError(f"Field {self.fieldId} not found in fields")
78
+ return part_cls(wh_field_name=field.systemName, value=self.text)
79
+ return part_cls(value=self.text)
80
+
81
+
82
+ class TagSchemaConstraint(BaseModel):
83
+ """
84
+ A class to define a constraint on an entity schema.
85
+ """
86
+
87
+ areUniqueResiduesCaseSensitive: bool | None = None
88
+ fields: list[TagSchemaFieldModel] | None = None
89
+ uniqueCanonicalSmilers: bool | None = None
90
+ uniqueResidues: bool | None = None
91
+
92
+ @classmethod
93
+ def from_constraint_fields(
94
+ cls, constraint_fields: list[TagSchemaFieldModel], bases: bool
95
+ ) -> TagSchemaConstraint:
96
+ """
97
+ Generates a Constraint object from a set of constraint fields to create a constraint on a schema.
98
+ """
99
+ return cls(
100
+ fields=constraint_fields,
101
+ uniqueResidues=bases,
102
+ uniqueCanonicalSmilers=False,
103
+ areUniqueResiduesCaseSensitive=False,
104
+ )
105
+
106
+
35
107
  class UpdateTagSchemaModel(BaseModel):
36
108
  """A pydantic model to define the input for the internal tag schema update endpoint."""
37
109
 
@@ -46,6 +118,9 @@ class UpdateTagSchemaModel(BaseModel):
46
118
  sequenceType: BenchlingSequenceType | None = None
47
119
  shouldCreateAsOligo: bool | None = None
48
120
  showResidues: bool | None = None
121
+ includeRegistryIdInChips: bool | None = None
122
+ useOrganizationCollectionAliasForDisplayLabel: bool | None = None
123
+ constraint: TagSchemaConstraint | None = None
49
124
 
50
125
 
51
126
  class CreateTagSchemaFieldModel(BaseModel):
@@ -243,7 +318,7 @@ class TagSchemaModel(BaseModel):
243
318
  authParentOption: str | None
244
319
  batchSchemaId: str | None
245
320
  childEntitySchemaSummaries: list[Any] | None
246
- constraint: Any | None
321
+ constraint: TagSchemaConstraint | None
247
322
  containableType: str | None
248
323
  fields: list[TagSchemaFieldModel]
249
324
  folderItemType: BenchlingFolderItemType
@@ -256,7 +331,7 @@ class TagSchemaModel(BaseModel):
256
331
  mixtureSchemaConfig: MixtureSchemaConfig | None
257
332
  name: str | None
258
333
  nameTemplateFields: list[str] | None
259
- nameTemplateParts: list[Any] | None
334
+ nameTemplateParts: list[NameTemplatePartModel] | None
260
335
  permissions: dict[str, bool] | None
261
336
  prefix: str | None
262
337
  registryId: str | None
@@ -299,7 +374,11 @@ class TagSchemaModel(BaseModel):
299
374
  if len(filtered_schemas) == len(wh_schema_names):
300
375
  break
301
376
  else:
302
- filtered_schemas = [cls.model_validate(schema) for schema in schemas_data]
377
+ for schema in schemas_data:
378
+ try:
379
+ filtered_schemas.append(cls.model_validate(schema))
380
+ except Exception as e:
381
+ print(f"Error validating schema {schema['sqlIdentifier']}: {e}")
303
382
  return filtered_schemas
304
383
 
305
384
  @classmethod
@@ -333,6 +412,11 @@ class TagSchemaModel(BaseModel):
333
412
  return field
334
413
  raise ValueError(f"Field '{wh_field_name}' not found in schema")
335
414
 
415
+ def get_internal_name_template_parts(self) -> list[NameTemplatePart]:
416
+ return [
417
+ part.to_name_template_part(self.fields) for part in self.nameTemplateParts
418
+ ]
419
+
336
420
  def update_schema_props(self, update_diff: dict[str, Any]) -> TagSchemaModel:
337
421
  """Updates the schema properties given the schema properties defined in code."""
338
422
  update_diff_names = list(update_diff.keys())
@@ -347,6 +431,46 @@ class TagSchemaModel(BaseModel):
347
431
  self.labelingStrategies = [o.value for o in update_props.naming_strategies]
348
432
  if "mixture_schema_config" in update_diff_names:
349
433
  self.mixtureSchemaConfig = update_props.mixture_schema_config
434
+ if "use_registry_id_as_label" in update_diff_names:
435
+ self.useOrganizationCollectionAliasForDisplayLabel = (
436
+ update_props.use_registry_id_as_label
437
+ )
438
+ if "include_registry_id_in_chips" in update_diff_names:
439
+ self.includeRegistryIdInChips = update_props.include_registry_id_in_chips
440
+
441
+ if "constraint_fields" in update_diff_names:
442
+ if update_props.constraint_fields:
443
+ has_bases = False
444
+ if "bases" in update_props.constraint_fields:
445
+ has_bases = True
446
+ update_props.constraint_fields.discard("bases")
447
+ constraint_fields = [
448
+ f
449
+ for f in self.fields
450
+ if f.systemName in update_props.constraint_fields
451
+ ]
452
+ self.constraint = TagSchemaConstraint.from_constraint_fields(
453
+ constraint_fields, has_bases
454
+ )
455
+ else:
456
+ self.constraint = None
457
+
458
+ if "constraint_fields" in update_diff_names:
459
+ if update_props.constraint_fields:
460
+ has_bases = False
461
+ if "bases" in update_props.constraint_fields:
462
+ has_bases = True
463
+ update_props.constraint_fields.discard("bases")
464
+ constraint_fields = [
465
+ f
466
+ for f in self.fields
467
+ if f.systemName in update_props.constraint_fields
468
+ ]
469
+ self.constraint = TagSchemaConstraint.from_constraint_fields(
470
+ constraint_fields, has_bases
471
+ )
472
+ else:
473
+ self.constraint = None
350
474
 
351
475
  self.prefix = (
352
476
  update_props.prefix if "prefix" in update_diff_names else self.prefix
@@ -359,6 +483,25 @@ class TagSchemaModel(BaseModel):
359
483
  self.name = update_props.name if "name" in update_diff_names else self.name
360
484
  return self
361
485
 
486
+ def update_name_template(
487
+ self, update_name_template: BaseNameTemplate
488
+ ) -> TagSchemaModel:
489
+ update_diff_names = update_name_template.model_dump(exclude_unset=True).keys()
490
+ self.nameTemplateParts = (
491
+ [
492
+ NameTemplatePartModel.from_name_template_part(part, self.fields)
493
+ for part in update_name_template.parts
494
+ ]
495
+ if "parts" in update_diff_names
496
+ else self.nameTemplateParts
497
+ )
498
+ self.shouldOrderNamePartsBySequence = (
499
+ update_name_template.order_name_parts_by_sequence
500
+ if "order_name_parts_by_sequence" in update_diff_names
501
+ else self.shouldOrderNamePartsBySequence
502
+ )
503
+ return self
504
+
362
505
  def update_field(
363
506
  self,
364
507
  benchling_service: BenchlingService,
@@ -9,6 +9,7 @@ from liminal.mappers import (
9
9
  convert_api_entity_type_to_entity_type,
10
10
  convert_api_field_type_to_field_type,
11
11
  )
12
+ from liminal.orm.name_template import NameTemplate
12
13
  from liminal.orm.schema_properties import MixtureSchemaConfig, SchemaProperties
13
14
 
14
15
 
@@ -16,7 +17,7 @@ def get_converted_tag_schemas(
16
17
  benchling_service: BenchlingService,
17
18
  include_archived: bool = False,
18
19
  wh_schema_names: set[str] | None = None,
19
- ) -> list[tuple[SchemaProperties, dict[str, BaseFieldProperties]]]:
20
+ ) -> list[tuple[SchemaProperties, NameTemplate, dict[str, BaseFieldProperties]]]:
20
21
  """This functions gets all Tag schemas from Benchling and converts them to our internal representation of a schema and its fields.
21
22
  It parses the Tag Schema and creates SchemaProperties and a list of FieldProperties for each field in the schema.
22
23
  If include_archived is True, it will include archived schemas and archived fields.
@@ -40,10 +41,15 @@ def convert_tag_schema_to_internal_schema(
40
41
  tag_schema: TagSchemaModel,
41
42
  dropdowns_map: dict[str, str],
42
43
  include_archived_fields: bool = False,
43
- ) -> tuple[SchemaProperties, dict[str, BaseFieldProperties]]:
44
+ ) -> tuple[SchemaProperties, NameTemplate, dict[str, BaseFieldProperties]]:
44
45
  all_fields = tag_schema.allFields
45
46
  if not include_archived_fields:
46
47
  all_fields = [f for f in all_fields if not f.archiveRecord]
48
+ constraint_fields: set[str] | None = None
49
+ if tag_schema.constraint:
50
+ constraint_fields = set([f.systemName for f in tag_schema.constraint.fields])
51
+ if tag_schema.constraint.uniqueResidues:
52
+ constraint_fields.add("bases")
47
53
  return (
48
54
  SchemaProperties(
49
55
  name=tag_schema.name,
@@ -63,7 +69,14 @@ def convert_tag_schema_to_internal_schema(
63
69
  BenchlingNamingStrategy(strategy)
64
70
  for strategy in tag_schema.labelingStrategies
65
71
  ),
72
+ constraint_fields=constraint_fields,
66
73
  _archived=tag_schema.archiveRecord is not None,
74
+ use_registry_id_as_label=tag_schema.useOrganizationCollectionAliasForDisplayLabel,
75
+ include_registry_id_in_chips=tag_schema.includeRegistryIdInChips,
76
+ ),
77
+ NameTemplate(
78
+ parts=tag_schema.get_internal_name_template_parts(),
79
+ order_name_parts_by_sequence=tag_schema.shouldOrderNamePartsBySequence,
67
80
  ),
68
81
  {
69
82
  f.systemName: convert_tag_schema_field_to_field_properties(f, dropdowns_map)
liminal/enums/__init__.py CHANGED
@@ -4,5 +4,4 @@ from liminal.enums.benchling_entity_type import BenchlingEntityType
4
4
  from liminal.enums.benchling_field_type import BenchlingFieldType
5
5
  from liminal.enums.benchling_folder_item_type import BenchlingFolderItemType
6
6
  from liminal.enums.benchling_naming_strategy import BenchlingNamingStrategy
7
- from liminal.enums.benchling_report_level import BenchlingReportLevel
8
7
  from liminal.enums.benchling_sequence_type import BenchlingSequenceType
@@ -13,3 +13,11 @@ class BenchlingEntityType(StrEnum):
13
13
  ENTRY = "entry"
14
14
  MIXTURE = "mixture"
15
15
  MOLECULE = "molecule"
16
+
17
+ def is_sequence(self) -> bool:
18
+ return self in [
19
+ self.DNA_SEQUENCE,
20
+ self.RNA_SEQUENCE,
21
+ self.DNA_OLIGO,
22
+ self.RNA_OLIGO,
23
+ ]
@@ -0,0 +1,12 @@
1
+ from liminal.base.str_enum import StrEnum
2
+
3
+
4
+ class NameTemplatePartType(StrEnum):
5
+ SEPARATOR = "SEPARATOR"
6
+ TEXT = "TEXT"
7
+ CREATION_YEAR = "CREATED_AT_YEAR"
8
+ CREATION_DATE = "CREATED_AT_DATE"
9
+ FIELD = "FIELD"
10
+ REGISTRY_IDENTIFIER_NUMBER = "REGISTRY_IDENTIFIER_NUMBER"
11
+ PROJECT = "PROJECT"
12
+ CHILD_ENTITY_LOT_NUMBER = "CHILD_ENTITY_LOT_NUMBER"
@@ -1,6 +1,16 @@
1
1
  # flake8: noqa: F401
2
2
  from liminal.base.base_operation import BaseOperation
3
+ from liminal.base.name_template_parts import (
4
+ CreationDatePart,
5
+ CreationYearPart,
6
+ FieldPart,
7
+ ProjectPart,
8
+ RegistryIdentifierNumberPart,
9
+ SeparatorPart,
10
+ TextPart,
11
+ )
3
12
  from liminal.base.properties.base_field_properties import BaseFieldProperties
13
+ from liminal.base.properties.base_name_template import BaseNameTemplate
4
14
  from liminal.base.properties.base_schema_properties import BaseSchemaProperties
5
15
  from liminal.dropdowns.operations import (
6
16
  ArchiveDropdown,
@@ -22,6 +32,7 @@ from liminal.entity_schemas.operations import (
22
32
  UnarchiveEntitySchemaField,
23
33
  UpdateEntitySchema,
24
34
  UpdateEntitySchemaField,
35
+ UpdateEntitySchemaNameTemplate,
25
36
  )
26
37
  from liminal.enums import (
27
38
  BenchlingAPIFieldType,
@@ -29,6 +40,5 @@ from liminal.enums import (
29
40
  BenchlingFieldType,
30
41
  BenchlingFolderItemType,
31
42
  BenchlingNamingStrategy,
32
- BenchlingReportLevel,
33
43
  BenchlingSequenceType,
34
44
  )
@@ -30,7 +30,8 @@ class RevisionsTimeline:
30
30
  )
31
31
  all_raw_revisions: list[Revision] = []
32
32
  for file_path in versions_dir_path.iterdir():
33
- all_raw_revisions.append(Revision.parse_from_file(file_path))
33
+ if file_path.is_file() and file_path.suffix == ".py":
34
+ all_raw_revisions.append(Revision.parse_from_file(file_path))
34
35
  self.revisions_map = self.validate_revisions(all_raw_revisions)
35
36
 
36
37
  def get_first_revision(self) -> Revision: