Compare commits
3 Commits
ccbc44e362
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c286239a7e | |||
| 4b838383ed | |||
| b4b0a7f72d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
/vscode
|
/vscode
|
||||||
__pycache__
|
__pycache__
|
||||||
/test_data.json
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ Libs used for benchmark:
|
|||||||
|
|
||||||
- [pydantic 2.4.2](https://docs.pydantic.dev/latest/)
|
- [pydantic 2.4.2](https://docs.pydantic.dev/latest/)
|
||||||
- [msgspec 0.18.4](https://jcristharif.com/msgspec/index.html)
|
- [msgspec 0.18.4](https://jcristharif.com/msgspec/index.html)
|
||||||
|
- [attrs 23.1.0](https://www.attrs.org)
|
||||||
|
|
||||||
Note that Pydantic used in benchmark is v2 which should be dozen of times faster than Pydantic v1. I have no time to waste benchmarking Pydantic v1.
|
Note that Pydantic used in benchmark is v2 which should be dozen of times faster than Pydantic v1. I have no time to waste benchmarking Pydantic v1.
|
||||||
|
|
||||||
@ -31,11 +32,15 @@ python main.py
|
|||||||
|
|
||||||
## Results
|
## Results
|
||||||
|
|
||||||
|
Test file: json, 162MB
|
||||||
|
|
||||||
```
|
```
|
||||||
*** Running PydanticBenchmark
|
*** Running PydanticBenchmark
|
||||||
Finished in 10.781s
|
Finished in 10.733s
|
||||||
|
*** Running AttrsBenchmark
|
||||||
|
Finished in 3.271s
|
||||||
*** Running MsgSpecBenchmark
|
*** Running MsgSpecBenchmark
|
||||||
Finished in 1.725s
|
Finished in 1.685s
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
0
benchmark/attrs_benchmark/__init__.py
Normal file
0
benchmark/attrs_benchmark/__init__.py
Normal file
10
benchmark/attrs_benchmark/benchmark.py
Normal file
10
benchmark/attrs_benchmark/benchmark.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import json
|
||||||
|
from benchmark.base import BenchmarkBase
|
||||||
|
from benchmark.attrs_benchmark.models import PlanogramsBulkInputPayload
|
||||||
|
|
||||||
|
|
||||||
|
class AttrsBenchmark(BenchmarkBase):
|
||||||
|
def _benchmark(self) -> None:
|
||||||
|
test_data = self._read_test_file()
|
||||||
|
json_data = json.loads(test_data)
|
||||||
|
data = PlanogramsBulkInputPayload(**json_data)
|
||||||
58
benchmark/attrs_benchmark/models.py
Normal file
58
benchmark/attrs_benchmark/models.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
from attr import define, field
|
||||||
|
import attrs
|
||||||
|
|
||||||
|
|
||||||
|
class ColumnItemType(str, Enum):
|
||||||
|
PRODUCT = "PRODUCT"
|
||||||
|
COMPONENT = "COMPONENT"
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class CorrelationId:
|
||||||
|
correlation_id: str = field(factory=lambda: uuid4().hex)
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class ColumnsInput:
|
||||||
|
column_number: int = field(
|
||||||
|
default=None, validator=[attrs.validators.ge(0), attrs.validators.lt(32767)]
|
||||||
|
)
|
||||||
|
external_product_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.min_len(1), attrs.validators.max_len(32)],
|
||||||
|
)
|
||||||
|
old_qty: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.ge(0), attrs.validators.lt(2147483647)],
|
||||||
|
)
|
||||||
|
new_qty: Optional[int] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.ge(0), attrs.validators.lt(2147483647)],
|
||||||
|
)
|
||||||
|
old_price: Optional[float] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.ge(0), attrs.validators.lt(99999999.99)],
|
||||||
|
)
|
||||||
|
new_price: Optional[float] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.ge(0), attrs.validators.lt(99999999.99)],
|
||||||
|
)
|
||||||
|
select_map: Optional[list[int]] = field(default=None)
|
||||||
|
item_type: Optional[ColumnItemType] = field(factory=lambda: ColumnItemType.PRODUCT)
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class PlanogramInput(CorrelationId):
|
||||||
|
machine_external_id: Optional[str] = field(
|
||||||
|
default=None,
|
||||||
|
validator=[attrs.validators.min_len(1), attrs.validators.max_len(32)],
|
||||||
|
)
|
||||||
|
columns: list[ColumnsInput] = field(factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class PlanogramsBulkInputPayload:
|
||||||
|
planograms: list[PlanogramInput] = field(factory=list)
|
||||||
@ -21,12 +21,12 @@ class CorrelationId(Struct, rename="camel"):
|
|||||||
|
|
||||||
class ColumnsInput(Struct, rename="camel"):
|
class ColumnsInput(Struct, rename="camel"):
|
||||||
column_number: StrictSmallInt
|
column_number: StrictSmallInt
|
||||||
external_product_id: Optional[ExternalId] = field(default=None)
|
external_product_id: Optional[ExternalId] = None
|
||||||
old_qty: Optional[QuantityInt] = field(default_factory=lambda: None)
|
old_qty: Optional[QuantityInt] = None
|
||||||
new_qty: Optional[QuantityInt] = field(default_factory=lambda: None)
|
new_qty: Optional[QuantityInt] = None
|
||||||
old_price: Optional[PriceFloat] = field(default_factory=lambda: None)
|
old_price: Optional[PriceFloat] = None
|
||||||
new_price: Optional[PriceFloat] = field(default_factory=lambda: None)
|
new_price: Optional[PriceFloat] = None
|
||||||
select_map: Optional[list[StrictSmallInt]] = field(default_factory=lambda: None)
|
select_map: Optional[list[StrictSmallInt]] = None
|
||||||
item_type: Optional[ColumnItemType] = field(
|
item_type: Optional[ColumnItemType] = field(
|
||||||
default_factory=lambda: ColumnItemType.PRODUCT
|
default_factory=lambda: ColumnItemType.PRODUCT
|
||||||
)
|
)
|
||||||
@ -36,16 +36,6 @@ class PlanogramInput(CorrelationId, Struct, rename="camel"):
|
|||||||
machine_external_id: ExternalId = field(default="")
|
machine_external_id: ExternalId = field(default="")
|
||||||
columns: list[ColumnsInput] = field(default_factory=list)
|
columns: list[ColumnsInput] = field(default_factory=list)
|
||||||
|
|
||||||
# class Config:
|
|
||||||
# title = "Planogram"
|
|
||||||
# alias_generator = to_camel_case
|
|
||||||
# populate_by_name = True
|
|
||||||
# str_strip_whitespace = True
|
|
||||||
|
|
||||||
|
|
||||||
class PlanogramsBulkInputPayload(Struct, rename="camel"):
|
class PlanogramsBulkInputPayload(Struct, rename="camel"):
|
||||||
planograms: list[PlanogramInput] = field(default_factory=list)
|
planograms: list[PlanogramInput] = field(default_factory=list)
|
||||||
|
|
||||||
# class Config:
|
|
||||||
# populate_by_name = True
|
|
||||||
# alias_generator = to_camel_case
|
|
||||||
|
|||||||
@ -25,22 +25,19 @@ class CorrelationId(BaseModel):
|
|||||||
|
|
||||||
class ColumnsInput(BaseModel):
|
class ColumnsInput(BaseModel):
|
||||||
column_number: StrictSmallInt = Field(
|
column_number: StrictSmallInt = Field(
|
||||||
alias="columnNumber", description="View index in Televend"
|
description="View index in Televend",
|
||||||
)
|
)
|
||||||
external_product_id: Optional[str] = Field(
|
external_product_id: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
alias="externalProductId",
|
|
||||||
description="Product or Component external ID used for product/component identification in external partner's ERP system",
|
description="Product or Component external ID used for product/component identification in external partner's ERP system",
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
)
|
)
|
||||||
old_qty: Optional[QuantityInt] = Field(
|
old_qty: Optional[QuantityInt] = Field(
|
||||||
default_factory=lambda: None,
|
default_factory=lambda: None,
|
||||||
alias="oldQty",
|
|
||||||
)
|
)
|
||||||
new_qty: Optional[QuantityInt] = Field(
|
new_qty: Optional[QuantityInt] = Field(
|
||||||
default_factory=lambda: None,
|
default_factory=lambda: None,
|
||||||
alias="newQty",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
old_price: Optional[float] = Field(
|
old_price: Optional[float] = Field(
|
||||||
@ -56,13 +53,10 @@ class ColumnsInput(BaseModel):
|
|||||||
le=99999999.99,
|
le=99999999.99,
|
||||||
)
|
)
|
||||||
|
|
||||||
select_map: Optional[List[StrictSmallInt]] = Field(
|
select_map: Optional[List[StrictSmallInt]] = Field(default_factory=lambda: None)
|
||||||
default_factory=lambda: None, alias="selectMap"
|
|
||||||
)
|
|
||||||
|
|
||||||
item_type: Optional[ColumnItemType] = Field(
|
item_type: Optional[ColumnItemType] = Field(
|
||||||
default_factory=lambda: ColumnItemType.PRODUCT,
|
default_factory=lambda: ColumnItemType.PRODUCT,
|
||||||
alias="itemType",
|
|
||||||
description="MUST be set if item is COMPONENT",
|
description="MUST be set if item is COMPONENT",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,27 +65,6 @@ class ColumnsInput(BaseModel):
|
|||||||
alias_generator = to_camel_case
|
alias_generator = to_camel_case
|
||||||
str_strip_whitespace = True
|
str_strip_whitespace = True
|
||||||
|
|
||||||
# @root_validator
|
|
||||||
# def check_required_fields(cls, values):
|
|
||||||
# if values.get("external_product_id"):
|
|
||||||
# if values.get("old_qty") is None:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"provide oldQty for product {values['external_product_id']}"
|
|
||||||
# )
|
|
||||||
# if values.get("new_qty") is None:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"provide newQty for product {values['external_product_id']}"
|
|
||||||
# )
|
|
||||||
# if values.get("old_price") is None:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"provide oldPrc for product {values['external_product_id']}"
|
|
||||||
# )
|
|
||||||
# if values.get("new_price") is None:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"provide newPrc for product {values['external_product_id']}"
|
|
||||||
# )
|
|
||||||
# return values
|
|
||||||
|
|
||||||
@validator("item_type")
|
@validator("item_type")
|
||||||
def set_item_type(cls, value):
|
def set_item_type(cls, value):
|
||||||
return value or ColumnItemType.PRODUCT
|
return value or ColumnItemType.PRODUCT
|
||||||
@ -99,7 +72,6 @@ class ColumnsInput(BaseModel):
|
|||||||
|
|
||||||
class PlanogramInput(CorrelationId, BaseModel):
|
class PlanogramInput(CorrelationId, BaseModel):
|
||||||
machine_external_id: str = Field(
|
machine_external_id: str = Field(
|
||||||
alias="machineExternalId",
|
|
||||||
description="Machine external ID",
|
description="Machine external ID",
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=32,
|
max_length=32,
|
||||||
@ -124,15 +96,3 @@ class PlanogramsBulkInputPayload(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
populate_by_name = True
|
populate_by_name = True
|
||||||
alias_generator = to_camel_case
|
alias_generator = to_camel_case
|
||||||
|
|
||||||
# @root_validator
|
|
||||||
# def check_machine_unique(cls, values):
|
|
||||||
# planograms = values.get("planograms", [])
|
|
||||||
# external_ids = set()
|
|
||||||
# for planogram in planograms:
|
|
||||||
# if planogram.machine_external_id in external_ids:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"Machine externalId must be unique! Duplicate {planogram.machine_external_id}"
|
|
||||||
# )
|
|
||||||
# external_ids.add(planogram.machine_external_id)
|
|
||||||
# return values
|
|
||||||
|
|||||||
16
main.py
16
main.py
@ -1,20 +1,26 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from benchmark.attrs_benchmark.benchmark import AttrsBenchmark
|
||||||
from benchmark.factories import create_test_file
|
from benchmark.factories import create_test_file
|
||||||
from benchmark.msgspec_benchmark.benchmark import MsgSpecBenchmark
|
from benchmark.msgspec_benchmark.benchmark import MsgSpecBenchmark
|
||||||
from benchmark.pydantic_benchmark.benchmark import PydanticBenchmark
|
from benchmark.pydantic_benchmark.benchmark import PydanticBenchmark
|
||||||
|
|
||||||
|
|
||||||
TEST_DATA_FILE = Path("test_data.json")
|
TEST_DATA_FILE = Path("test_data.json")
|
||||||
|
BIG_TEST_DATA_FILE = Path("test_data-big.json")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main(test_file: Path) -> None:
|
||||||
pydantic_benchmark = PydanticBenchmark(TEST_DATA_FILE)
|
pydantic_benchmark = PydanticBenchmark(test_file)
|
||||||
pydantic_benchmark.execute()
|
pydantic_benchmark.execute()
|
||||||
|
|
||||||
msgspec_benchmark = MsgSpecBenchmark(TEST_DATA_FILE)
|
attrs_benchmark = AttrsBenchmark(test_file)
|
||||||
|
attrs_benchmark.execute()
|
||||||
|
|
||||||
|
msgspec_benchmark = MsgSpecBenchmark(test_file)
|
||||||
msgspec_benchmark.execute()
|
msgspec_benchmark.execute()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# create_test_file(TEST_DATA_FILE)
|
# create_test_file(BIG_TEST_DATA_FILE)
|
||||||
main()
|
main(BIG_TEST_DATA_FILE)
|
||||||
|
|||||||
20
poetry.lock
generated
20
poetry.lock
generated
@ -11,6 +11,24 @@ files = [
|
|||||||
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
{file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "23.1.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
|
||||||
|
{file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
|
||||||
|
dev = ["attrs[docs,tests]", "pre-commit"]
|
||||||
|
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
||||||
|
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
||||||
|
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "faker"
|
name = "faker"
|
||||||
version = "19.9.0"
|
version = "19.9.0"
|
||||||
@ -277,4 +295,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "f67f7e3c078da6291615ead8743e8eb520d31e62bab449b5cd5f3e9b3b3f540e"
|
content-hash = "43640240ca1c3ae251a946f70979654201b8adac0d9a770c92d64deb83d13610"
|
||||||
|
|||||||
@ -11,6 +11,7 @@ python = "^3.11"
|
|||||||
pydantic = "^2.4.2"
|
pydantic = "^2.4.2"
|
||||||
msgspec = "^0.18.4"
|
msgspec = "^0.18.4"
|
||||||
polyfactory = "^2.9.0"
|
polyfactory = "^2.9.0"
|
||||||
|
attrs = "^23.1.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
1
test_data-big.json
Normal file
1
test_data-big.json
Normal file
File diff suppressed because one or more lines are too long
1
test_data.json
Normal file
1
test_data.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user