openepd 5.0.0__py3-none-any.whl → 5.1.1__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 (61) hide show
  1. openepd/__version__.py +1 -1
  2. openepd/model/common.py +39 -0
  3. openepd/model/declaration.py +7 -2
  4. openepd/model/geography.py +1 -1
  5. openepd/model/lcia.py +6 -3
  6. openepd/model/org.py +0 -1
  7. openepd/model/pcr.py +2 -2
  8. openepd/model/specs/README.md +34 -8
  9. openepd/model/specs/asphalt.py +2 -2
  10. openepd/model/specs/base.py +13 -0
  11. openepd/model/specs/generated/__init__.py +80 -0
  12. openepd/model/specs/generated/cladding.py +4 -4
  13. openepd/model/specs/generated/concrete.py +8 -7
  14. openepd/model/specs/generated/electrical.py +2 -2
  15. openepd/model/specs/generated/finishes.py +10 -6
  16. openepd/model/specs/generated/masonry.py +6 -2
  17. openepd/model/specs/generated/network_infrastructure.py +7 -2
  18. openepd/model/specs/generated/openings.py +10 -6
  19. openepd/model/specs/generated/sheathing.py +8 -4
  20. openepd/model/specs/generated/steel.py +10 -5
  21. openepd/model/specs/range/__init__.py +101 -0
  22. openepd/model/specs/range/accessories.py +97 -0
  23. openepd/model/specs/range/aggregates.py +57 -0
  24. openepd/model/specs/range/aluminium.py +92 -0
  25. openepd/model/specs/range/asphalt.py +61 -0
  26. openepd/model/specs/range/bulk_materials.py +31 -0
  27. openepd/model/specs/range/cast_decks_and_underlayment.py +34 -0
  28. openepd/model/specs/range/cladding.py +275 -0
  29. openepd/model/specs/range/cmu.py +44 -0
  30. openepd/model/specs/range/concrete.py +179 -0
  31. openepd/model/specs/range/conveying_equipment.py +86 -0
  32. openepd/model/specs/range/electrical.py +422 -0
  33. openepd/model/specs/range/electrical_transmission_and_distribution_equipment.py +96 -0
  34. openepd/model/specs/range/electricity.py +31 -0
  35. openepd/model/specs/range/finishes.py +585 -0
  36. openepd/model/specs/range/fire_and_smoke_protection.py +108 -0
  37. openepd/model/specs/range/furnishings.py +137 -0
  38. openepd/model/specs/range/grouting.py +34 -0
  39. openepd/model/specs/range/manufacturing_inputs.py +190 -0
  40. openepd/model/specs/range/masonry.py +87 -0
  41. openepd/model/specs/range/material_handling.py +50 -0
  42. openepd/model/specs/range/mechanical.py +307 -0
  43. openepd/model/specs/range/mechanical_insulation.py +42 -0
  44. openepd/model/specs/range/network_infrastructure.py +208 -0
  45. openepd/model/specs/range/openings.py +512 -0
  46. openepd/model/specs/range/other_electrical_equipment.py +31 -0
  47. openepd/model/specs/range/other_materials.py +194 -0
  48. openepd/model/specs/range/plumbing.py +200 -0
  49. openepd/model/specs/range/precast_concrete.py +115 -0
  50. openepd/model/specs/range/sheathing.py +86 -0
  51. openepd/model/specs/range/steel.py +332 -0
  52. openepd/model/specs/range/thermal_moisture_protection.py +336 -0
  53. openepd/model/specs/range/utility_piping.py +75 -0
  54. openepd/model/specs/range/wood.py +228 -0
  55. openepd/model/specs/range/wood_joists.py +44 -0
  56. openepd/model/validation/numbers.py +11 -5
  57. openepd/model/validation/quantity.py +440 -52
  58. {openepd-5.0.0.dist-info → openepd-5.1.1.dist-info}/METADATA +6 -1
  59. {openepd-5.0.0.dist-info → openepd-5.1.1.dist-info}/RECORD +61 -26
  60. {openepd-5.0.0.dist-info → openepd-5.1.1.dist-info}/LICENSE +0 -0
  61. {openepd-5.0.0.dist-info → openepd-5.1.1.dist-info}/WHEEL +0 -0
@@ -17,12 +17,11 @@ from abc import ABC, abstractmethod
17
17
  from typing import TYPE_CHECKING, Any, Callable, ClassVar
18
18
 
19
19
  from openepd.compat.pydantic import pyd
20
- from openepd.model.common import Amount, OpenEPDUnit
20
+ from openepd.model.base import BaseOpenEpdSchema
21
+ from openepd.model.common import Amount, OpenEPDUnit, RangeAmount
21
22
 
22
23
  if TYPE_CHECKING:
23
- from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
24
-
25
- QuantityValidatorType = Callable[[type[BaseOpenEpdHierarchicalSpec], str], str]
24
+ QuantityValidatorType = Callable[[type, str], str]
26
25
 
27
26
 
28
27
  class QuantityValidator(ABC):
@@ -104,42 +103,42 @@ class ExternalValidationConfig:
104
103
 
105
104
 
106
105
  def validate_unit_factory(dimensionality: OpenEPDUnit | str) -> "QuantityValidatorType":
107
- """Create validator for quantity field to check unit matching."""
106
+ """Create validator for units (not quantities) to check for dimensionality."""
108
107
 
109
- def validator(cls: "type[BaseOpenEpdHierarchicalSpec]", value: str) -> str:
108
+ def validator(cls: type | None, value: str) -> str:
110
109
  if ExternalValidationConfig.QUANTITY_VALIDATOR is not None:
111
- ExternalValidationConfig.QUANTITY_VALIDATOR.validate_unit_correctness(value, dimensionality)
110
+ ExternalValidationConfig.QUANTITY_VALIDATOR.validate_same_dimensionality(value, dimensionality)
112
111
  return value
113
112
 
114
113
  return validator
115
114
 
116
115
 
117
- def validate_quantity_ge_factory(min_value: str) -> "QuantityValidatorType":
118
- """Create validator to check that quantity is greater than or equal to min_value."""
116
+ def validate_quantity_unit_factory(dimensionality: OpenEPDUnit | str) -> "QuantityValidatorType":
117
+ """Create validator for quantity field to check unit matching."""
119
118
 
120
- def validator(cls: "type[BaseOpenEpdHierarchicalSpec]", value: str) -> str:
119
+ def validator(cls: type | None, value: str) -> str:
121
120
  if ExternalValidationConfig.QUANTITY_VALIDATOR is not None:
122
- ExternalValidationConfig.QUANTITY_VALIDATOR.validate_quantity_greater_or_equal(value, min_value)
121
+ ExternalValidationConfig.QUANTITY_VALIDATOR.validate_unit_correctness(value, dimensionality)
123
122
  return value
124
123
 
125
124
  return validator
126
125
 
127
126
 
128
- def validate_quantity_le_factory(max_value: str) -> "QuantityValidatorType":
129
- """Create validator to check that quantity is less than or equal to max_value."""
127
+ def validate_quantity_ge_factory(min_value: str) -> "QuantityValidatorType":
128
+ """Create validator to check that quantity is greater than or equal to min_value."""
130
129
 
131
- def validator(cls: "type[BaseOpenEpdHierarchicalSpec]", value: str) -> str:
130
+ def validator(cls: type | None, value: str) -> str:
132
131
  if ExternalValidationConfig.QUANTITY_VALIDATOR is not None:
133
- ExternalValidationConfig.QUANTITY_VALIDATOR.validate_quantity_less_or_equal(value, max_value)
132
+ ExternalValidationConfig.QUANTITY_VALIDATOR.validate_quantity_greater_or_equal(value, min_value)
134
133
  return value
135
134
 
136
135
  return validator
137
136
 
138
137
 
139
- def validate_quantity_for_new_validator(max_value: str) -> Callable:
138
+ def validate_quantity_le_factory(max_value: str) -> "QuantityValidatorType":
140
139
  """Create validator to check that quantity is less than or equal to max_value."""
141
140
 
142
- def validator(value: str) -> str:
141
+ def validator(cls: type | None, value: str) -> str:
143
142
  if ExternalValidationConfig.QUANTITY_VALIDATOR is not None:
144
143
  ExternalValidationConfig.QUANTITY_VALIDATOR.validate_quantity_less_or_equal(value, max_value)
145
144
  return value
@@ -151,38 +150,6 @@ def validate_quantity_for_new_validator(max_value: str) -> Callable:
151
150
  # todo these types should be replaced by Annotated[str, AfterValidator...] as we move completely to pydantic 2
152
151
 
153
152
 
154
- class AmountWithDimensionality(Amount, ABC):
155
- """Class for dimensionality-validated amounts."""
156
-
157
- dimensionality_unit: ClassVar[str | None] = None
158
-
159
- # Unit for dimensionality to validate against, for example "kg"
160
-
161
- @pyd.root_validator
162
- def check_dimensionality_matches(cls, values: dict[str, Any]) -> dict[str, Any]:
163
- """Check that this amount conforms to the same dimensionality as dimensionality_unit."""
164
- if not cls.dimensionality_unit:
165
- return values
166
-
167
- from openepd.model.specs.base import BaseOpenEpdHierarchicalSpec
168
-
169
- str_repr = f"{values['qty']} {values['unit']}"
170
- validate_unit_factory(cls.dimensionality_unit)(BaseOpenEpdHierarchicalSpec, str_repr)
171
- return values
172
-
173
-
174
- class AmountMass(AmountWithDimensionality):
175
- """Amount of mass, measured in kg, t, etc."""
176
-
177
- dimensionality_unit = OpenEPDUnit.kg
178
-
179
-
180
- class AmountGWP(AmountWithDimensionality):
181
- """Amount of Global Warming Potential, measured in kgCO2e."""
182
-
183
- dimensionality_unit = OpenEPDUnit.kg_co2
184
-
185
-
186
153
  class QuantityStr(str):
187
154
  """
188
155
  Quantity string type.
@@ -196,8 +163,10 @@ class QuantityStr(str):
196
163
 
197
164
  @classmethod
198
165
  def __get_validators__(cls):
199
- yield validate_unit_factory(cls.unit)
200
- yield validate_quantity_ge_factory(f"0 {cls.unit}")
166
+ unit = getattr(cls, "unit", None)
167
+ if unit:
168
+ yield validate_quantity_unit_factory(cls.unit)
169
+ yield validate_quantity_ge_factory(f"0 {cls.unit}")
201
170
 
202
171
  @classmethod
203
172
  def __modify_schema__(cls, field_schema):
@@ -245,7 +214,7 @@ class LengthMmStr(QuantityStr):
245
214
  class LengthInchStr(QuantityStr):
246
215
  """Length (inch) quantity type."""
247
216
 
248
- unit = OpenEPDUnit.m
217
+ unit = "inch"
249
218
 
250
219
  @classmethod
251
220
  def __modify_schema__(cls, field_schema):
@@ -336,3 +305,422 @@ class AreaPerVolumeStr(QuantityStr):
336
305
  """Area per unit of volume quantity type."""
337
306
 
338
307
  unit = "m2 / l"
308
+
309
+
310
+ class WithDimensionalityMixin(BaseOpenEpdSchema):
311
+ """Class for dimensionality-validated amounts."""
312
+
313
+ dimensionality_unit: ClassVar[str | None] = None
314
+
315
+ # Unit for dimensionality to validate against, for example "kg"
316
+
317
+ @pyd.root_validator
318
+ def check_dimensionality_matches(cls, values: dict[str, Any]) -> dict[str, Any]:
319
+ """Check that this amount conforms to the same dimensionality as dimensionality_unit."""
320
+ if not cls.dimensionality_unit:
321
+ return values
322
+
323
+ validate_unit_factory(cls.dimensionality_unit)(BaseOpenEpdSchema, values.get("unit")) # type:ignore [arg-type]
324
+ return values
325
+
326
+
327
+ class AmountRangeWithDimensionality(RangeAmount, WithDimensionalityMixin):
328
+ """Mass amount, range."""
329
+
330
+ class Config:
331
+ """Pydantic config."""
332
+
333
+ @staticmethod
334
+ def schema_extra(schema: dict[str, Any], model: type["AmountRangeWithDimensionality"]) -> None:
335
+ """Modify json schema."""
336
+ schema["example"] = {"min": 1.2, "max": 3.4, "unit": str(model.dimensionality_unit) or None}
337
+
338
+
339
+ class WithMassKgMixin(WithDimensionalityMixin):
340
+ """Unit validation mixin."""
341
+
342
+ dimensionality_unit = MassKgStr.unit
343
+
344
+
345
+ class AmountMass(Amount, WithMassKgMixin):
346
+ """Amount of mass, measured in kg, t, etc."""
347
+
348
+ pass
349
+
350
+
351
+ class AmountRangeMass(AmountRangeWithDimensionality, WithMassKgMixin):
352
+ """Range of masses."""
353
+
354
+ pass
355
+
356
+
357
+ class WithGwpMixin(WithDimensionalityMixin):
358
+ """Unit validation mixin."""
359
+
360
+ dimensionality_unit = GwpKgCo2eStr.unit
361
+
362
+
363
+ class AmountGWP(Amount, WithGwpMixin):
364
+ """Amount of Global Warming Potential, measured in kgCO2e."""
365
+
366
+ pass
367
+
368
+
369
+ class AmountRangeGWP(AmountRangeWithDimensionality, WithGwpMixin):
370
+ """Range of masses."""
371
+
372
+ pass
373
+
374
+
375
+ class WithLengthMMixin(WithDimensionalityMixin):
376
+ """Unit validation mixin."""
377
+
378
+ pass
379
+
380
+
381
+ class AmountRangeLengthM(AmountRangeWithDimensionality, WithLengthMMixin):
382
+ """Range of lengths (m)."""
383
+
384
+ pass
385
+
386
+
387
+ class AmountLengthM(Amount, WithLengthMMixin):
388
+ """Length (m)."""
389
+
390
+ pass
391
+
392
+
393
+ class WithLengthMmMixin(WithDimensionalityMixin):
394
+ """Unit validation mixin."""
395
+
396
+ dimensionality_unit = LengthMmStr.unit
397
+
398
+
399
+ class AmountLengthMm(Amount, WithLengthMMixin):
400
+ """Length (mm)."""
401
+
402
+ pass
403
+
404
+
405
+ class AmountRangeLengthMm(AmountRangeWithDimensionality, WithLengthMmMixin):
406
+ """Range of lengths (mm)."""
407
+
408
+ pass
409
+
410
+
411
+ class WithPressureMpaMixin(WithDimensionalityMixin):
412
+ """Unit validation mixin."""
413
+
414
+ dimensionality_unit = PressureMPaStr.unit
415
+
416
+
417
+ class AmountPressureMpa(Amount, WithPressureMpaMixin):
418
+ """Length (mm)."""
419
+
420
+ pass
421
+
422
+
423
+ class AmountRangePressureMpa(AmountRangeWithDimensionality, WithPressureMpaMixin):
424
+ """Range of lengths (mm)."""
425
+
426
+ pass
427
+
428
+
429
+ class WithAreaM2Mixin(WithDimensionalityMixin):
430
+ """Unit validation mixin."""
431
+
432
+ dimensionality_unit = AreaM2Str.unit
433
+
434
+
435
+ class AmountAreaM2(Amount, WithAreaM2Mixin):
436
+ """Area (m2)."""
437
+
438
+ pass
439
+
440
+
441
+ class AmountRangeAreaM2(AmountRangeWithDimensionality, WithAreaM2Mixin):
442
+ """Range of Area (m2)."""
443
+
444
+ pass
445
+
446
+
447
+ class WithLengthInchStr(WithDimensionalityMixin):
448
+ """Unit validation mixin."""
449
+
450
+ dimensionality_unit = LengthInchStr.unit
451
+
452
+
453
+ class AmountLengthInch(Amount, WithLengthInchStr):
454
+ """Length (inch)."""
455
+
456
+ pass
457
+
458
+
459
+ class AmountRangeLengthInch(AmountRangeWithDimensionality, WithLengthInchStr):
460
+ """Range of Length (inch)."""
461
+
462
+ pass
463
+
464
+
465
+ class WithTemperatureCMixin(WithDimensionalityMixin):
466
+ """Unit validation mixin."""
467
+
468
+ dimensionality_unit = TemperatureCStr.unit
469
+
470
+
471
+ class AmountTemperatureC(Amount, WithTemperatureCMixin):
472
+ """Temperature (degrees C)."""
473
+
474
+ pass
475
+
476
+
477
+ class AmountRangeTemperatureC(AmountRangeWithDimensionality, WithTemperatureCMixin):
478
+ """Range of Temperature (degrees C)."""
479
+
480
+ pass
481
+
482
+
483
+ class WithCapacityPerHourMixin(WithDimensionalityMixin):
484
+ """Unit validation mixin."""
485
+
486
+ dimensionality_unit = CapacityPerHourStr.unit
487
+
488
+
489
+ class AmountCapacityPerHour(Amount, WithCapacityPerHourMixin):
490
+ """Capacity per hour."""
491
+
492
+ pass
493
+
494
+
495
+ class AmountRangeCapacityPerHour(AmountRangeWithDimensionality, WithCapacityPerHourMixin):
496
+ """Capacity per hour range."""
497
+
498
+ pass
499
+
500
+
501
+ class WithRValueMixin(WithDimensionalityMixin):
502
+ """Unit validation mixin."""
503
+
504
+ dimensionality_unit = RValueStr.unit
505
+
506
+
507
+ class AmountRValue(Amount, WithRValueMixin):
508
+ """R-Value."""
509
+
510
+ pass
511
+
512
+
513
+ class AmountRangeRValue(AmountRangeWithDimensionality, WithRValueMixin):
514
+ """R-Value range."""
515
+
516
+ pass
517
+
518
+
519
+ class WithSpeedMixin(WithDimensionalityMixin):
520
+ """Unit validation mixin."""
521
+
522
+ dimensionality_unit = SpeedStr.unit
523
+
524
+
525
+ class AmountSpeed(Amount, WithSpeedMixin):
526
+ """Speed."""
527
+
528
+ pass
529
+
530
+
531
+ class AmountRangeSpeed(AmountRangeWithDimensionality, WithSpeedMixin):
532
+ """Speed range."""
533
+
534
+ pass
535
+
536
+
537
+ class WithColorTemperatureMixin(WithDimensionalityMixin):
538
+ """Unit validation mixin."""
539
+
540
+ dimensionality_unit = ColorTemperatureStr.unit
541
+
542
+
543
+ class AmountColorTemperature(Amount, WithColorTemperatureMixin):
544
+ """Color temperature."""
545
+
546
+ pass
547
+
548
+
549
+ class AmountRangeColorTemperature(AmountRangeWithDimensionality, WithColorTemperatureMixin):
550
+ """Color temperature range."""
551
+
552
+ pass
553
+
554
+
555
+ class WithLuminosityMixin(WithDimensionalityMixin):
556
+ """Unit validation mixin."""
557
+
558
+ dimensionality_unit = LuminosityStr.unit
559
+
560
+
561
+ class AmountLuminosity(Amount, WithLuminosityMixin):
562
+ """Luminosity."""
563
+
564
+ pass
565
+
566
+
567
+ class AmountRangeLuminosity(AmountRangeWithDimensionality, WithLuminosityMixin):
568
+ """Luminosity range."""
569
+
570
+ pass
571
+
572
+
573
+ class WithPowerMixin(WithDimensionalityMixin):
574
+ """Unit validation mixin."""
575
+
576
+ dimensionality_unit = PowerStr.unit
577
+
578
+
579
+ class AmountPower(Amount, WithPowerMixin):
580
+ """Power."""
581
+
582
+ pass
583
+
584
+
585
+ class AmountRangePower(AmountRangeWithDimensionality, WithPowerMixin):
586
+ """Power range."""
587
+
588
+ pass
589
+
590
+
591
+ class WithElectricalCurrentMixin(WithDimensionalityMixin):
592
+ """Unit validation mixin."""
593
+
594
+ dimensionality_unit = ElectricalCurrentStr.unit
595
+
596
+
597
+ class AmountElectricalCurrent(Amount, WithElectricalCurrentMixin):
598
+ """Current."""
599
+
600
+ pass
601
+
602
+
603
+ class AmountRangeElectricalCurrent(AmountRangeWithDimensionality, WithElectricalCurrentMixin):
604
+ """Current range."""
605
+
606
+ pass
607
+
608
+
609
+ class WithVolumeMixin(WithDimensionalityMixin):
610
+ """Unit validation mixin."""
611
+
612
+ dimensionality_unit = VolumeStr.unit
613
+
614
+
615
+ class AmountVolume(Amount, WithVolumeMixin):
616
+ """Volume."""
617
+
618
+ pass
619
+
620
+
621
+ class AmountRangeVolume(AmountRangeWithDimensionality, WithVolumeMixin):
622
+ """Volume range."""
623
+
624
+ pass
625
+
626
+
627
+ class WithAirflowMixin(WithDimensionalityMixin):
628
+ """Unit validation mixin."""
629
+
630
+ dimensionality_unit = AirflowStr.unit
631
+
632
+
633
+ class AmountAirflow(Amount, WithAirflowMixin):
634
+ """Airflow."""
635
+
636
+ pass
637
+
638
+
639
+ class AmountRangeAirflow(AmountRangeWithDimensionality, WithAirflowMixin):
640
+ """Airflow range."""
641
+
642
+ pass
643
+
644
+
645
+ class WithFlowRateMixin(WithDimensionalityMixin):
646
+ """Unit validation mixin."""
647
+
648
+ dimensionality_unit = FlowRateStr.unit
649
+
650
+
651
+ class AmountFlowRate(Amount, WithFlowRateMixin):
652
+ """Flow Rate."""
653
+
654
+ pass
655
+
656
+
657
+ class AmountRangeFlowRate(AmountRangeWithDimensionality, WithFlowRateMixin):
658
+ """Flow Rate range."""
659
+
660
+ pass
661
+
662
+
663
+ class WithMassPerLengthMixin(WithDimensionalityMixin):
664
+ """Unit validation mixin."""
665
+
666
+ dimensionality_unit = MassPerLengthStr.unit
667
+
668
+
669
+ class AmountMassPerLength(Amount, WithFlowRateMixin):
670
+ """Mass per length."""
671
+
672
+ pass
673
+
674
+
675
+ class AmountRangeMassPerLength(AmountRangeWithDimensionality, WithFlowRateMixin):
676
+ """Mass per length range."""
677
+
678
+ pass
679
+
680
+
681
+ class WithAreaPerVolumeMixin(WithDimensionalityMixin):
682
+ """Unit validation mixin."""
683
+
684
+ dimensionality_unit = AreaPerVolumeStr.unit
685
+
686
+
687
+ class AmountAreaPerVolume(Amount, WithFlowRateMixin):
688
+ """Area per volume."""
689
+
690
+ pass
691
+
692
+
693
+ class AmountRangeAreaPerVolume(AmountRangeWithDimensionality, WithFlowRateMixin):
694
+ """Area per volume range."""
695
+
696
+ pass
697
+
698
+
699
+ # known range amounts
700
+ SUPPORTED_RANGE_TYPES: tuple[type[AmountRangeWithDimensionality], ...] = (
701
+ AmountRangeMass,
702
+ AmountRangeGWP,
703
+ AmountRangeLengthM,
704
+ AmountRangeLengthMm,
705
+ AmountRangePressureMpa,
706
+ AmountRangeAreaM2,
707
+ AmountRangeLengthInch,
708
+ AmountRangeTemperatureC,
709
+ AmountRangeCapacityPerHour,
710
+ AmountRangeRValue,
711
+ AmountRangeSpeed,
712
+ AmountRangeColorTemperature,
713
+ AmountRangeLuminosity,
714
+ AmountRangePower,
715
+ AmountRangeElectricalCurrent,
716
+ AmountRangeVolume,
717
+ AmountRangeAirflow,
718
+ AmountRangeFlowRate,
719
+ AmountRangeMassPerLength,
720
+ AmountRangeAreaPerVolume,
721
+ )
722
+
723
+ # known range amount mapping by unit
724
+ SUPPORTED_RANGES_BY_UNIT: dict[str, type[AmountRangeWithDimensionality]] = {
725
+ str(t.dimensionality_unit): t for t in SUPPORTED_RANGE_TYPES
726
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openepd
3
- Version: 5.0.0
3
+ Version: 5.1.1
4
4
  Summary: Python library to work with OpenEPD format
5
5
  Home-page: https://github.com/cchangelabs/openepd
6
6
  License: Apache-2.0
@@ -170,6 +170,11 @@ codes, UN m49 codification, and special regions. To update the enums, first upda
170
170
  Windows is not supported for development. You can use WSL2 with Ubuntu 20.04 or higher.
171
171
  Instructions are the same as for regular GNU/Linux installation.
172
172
 
173
+ ### Commit messages
174
+
175
+ Commit messages should follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/#specification)
176
+ specification as we use automatic version with [commitizen](https://commitizen-tools.github.io/commitizen/).
177
+
173
178
  # Credits
174
179
 
175
180
  This library has been written and maintained by [C-Change Labs](https://c-change-labs.com/).