{"id":25418836,"url":"https://github.com/complexheart/py-criteria","last_synced_at":"2026-02-17T07:00:58.913Z","repository":{"id":39890321,"uuid":"467965791","full_name":"ComplexHeart/py-criteria","owner":"ComplexHeart","description":"Implementation of Criteria patterns in Python for Complex Heart SDK.","archived":false,"fork":false,"pushed_at":"2026-01-22T06:26:05.000Z","size":24,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-22T20:40:15.547Z","etag":null,"topics":["criteria","python","specification-pattern"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ComplexHeart.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-03-09T14:41:02.000Z","updated_at":"2026-01-22T06:26:09.000Z","dependencies_parsed_at":"2025-02-16T18:39:17.627Z","dependency_job_id":null,"html_url":"https://github.com/ComplexHeart/py-criteria","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/ComplexHeart/py-criteria","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ComplexHeart%2Fpy-criteria","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ComplexHeart%2Fpy-criteria/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ComplexHeart%2Fpy-criteria/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ComplexHeart%2Fpy-criteria/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ComplexHeart","download_url":"https://codeload.github.com/ComplexHeart/py-criteria/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ComplexHeart%2Fpy-criteria/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29536527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T05:00:25.817Z","status":"ssl_error","status_checked_at":"2026-02-17T04:57:16.126Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["criteria","python","specification-pattern"],"created_at":"2025-02-16T18:28:19.402Z","updated_at":"2026-02-17T07:00:58.903Z","avatar_url":"https://github.com/ComplexHeart.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# Criteria (a.k.a Filter)\n\n[![Test](https://github.com/ComplexHeart/py-criteria/actions/workflows/test.yml/badge.svg)](https://github.com/ComplexHeart/py-criteria/actions/workflows/test.yml)\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ComplexHeart_py-criteria\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=ComplexHeart_py-criteria)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ComplexHeart_py-criteria\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=ComplexHeart_py-criteria)\n\nSmall implementation of a filter criteria pattern in Python for Complex Heart SDK. Query-system agnostic - works with SQL, NoSQL, APIs, or any data source.\n\n## Installation\n\n```bash\npip install complex-heart-criteria\n```\n\nor using uv:\n\n```bash\nuv add complex-heart-criteria\n```\n\n## Usage\n\n```python\nfrom complexheart.domain.criteria import Criteria, Filter, FilterGroup, Order, Page\n```\n\n### Building FilterGroups\n\nUse `+` to compose filters into a FilterGroup (AND logic within group):\n\n```python\nactive_adults = Filter.equal(\"status\", \"active\") + Filter.greater_or_equal_than(\"age\", 18)\n```\n\nOr use the fluent API:\n\n```python\nactive_adults = (\n    FilterGroup.empty()\n    .add_filter_equal(\"status\", \"active\")\n    .add_filter_greater_or_equal_than(\"age\", 18)\n)\n```\n\n### Building Criteria\n\nAdd FilterGroups to Criteria (OR logic between groups):\n\n```python\nc = (\n    Criteria()\n    .with_filter_group(active_adults)\n    .with_order(Order.desc((\"created_at\",)))\n    .with_page_limit(10)\n    .with_page_offset(20)\n)\n```\n\n### Merging Criteria\n\nUse `|` for OR and `\u0026` for AND between Criteria objects:\n\n```python\nactive_users = Criteria().with_filter_group(\n    Filter.equal(\"status\", \"active\") + Filter.greater_than(\"age\", 18)\n)\nadmin_users = Criteria().with_filter_group(\n    FilterGroup.create(Filter.equal(\"role\", \"admin\"))\n)\n\n# OR: concatenate groups\ncombined = active_users | admin_users\n# Result: (status = 'active' AND age \u003e 18) OR (role = 'admin')\n\n# AND: merge into single group\nmerged = active_users \u0026 admin_users\n# Result: (status = 'active' AND age \u003e 18 AND role = 'admin')\n```\n\n### Fluent API\n\n```python\ncriteria = (\n    Criteria()\n    .filter(\"status\", \"==\", \"active\", group=0)\n    .filter(\"age\", \"\u003e=\", 18, group=0)\n    .filter(\"role\", \"==\", \"admin\", group=1)\n    .order_by((\"created_at\",), \"DESC\")\n    .limit(10)\n    .offset(20)\n)\n# Result: (status = 'active' AND age \u003e= 18) OR (role = 'admin')\n```\n\n### Filter Factories\n\n```python\nFilter.equal(\"name\", \"Vincent\")              # name == 'Vincent'\nFilter.not_equal(\"name\", \"Vincent\")          # name != 'Vincent'\nFilter.greater_than(\"age\", 18)               # age \u003e 18\nFilter.greater_or_equal_than(\"age\", 18)      # age \u003e= 18\nFilter.less_than(\"age\", 65)                  # age \u003c 65\nFilter.less_or_equal_than(\"age\", 65)         # age \u003c= 65\nFilter.in_(\"status\", [\"a\", \"b\"])             # status IN ('a', 'b')\nFilter.not_in(\"status\", [\"x\"])               # status NOT IN ('x')\nFilter.like(\"name\", \"Vin%\")                  # name LIKE 'Vin%'\nFilter.not_like(\"name\", \"Vin%\")              # name NOT LIKE 'Vin%'\nFilter.contains(\"tags\", \"vip\")               # tags contains 'vip'\nFilter.not_contains(\"tags\", \"x\")             # tags not contains 'x'\n```\n\n### FilterGroup Methods\n\n```python\ngroup = (\n    FilterGroup.empty()\n    .add_filter_equal(\"status\", \"active\")\n    .add_filter_greater_than(\"age\", 18)\n    .add_filter_in(\"role\", [\"admin\", \"moderator\"])\n)\n```\n\nAvailable methods: `add_filter_equal`, `add_filter_not_equal`, `add_filter_greater_than`,\n`add_filter_greater_or_equal_than`, `add_filter_less_than`, `add_filter_less_or_equal_than`,\n`add_filter_in`, `add_filter_not_in`, `add_filter_like`, `add_filter_not_like`,\n`add_filter_contains`, `add_filter_not_contains`.\n\n### Repository Integration\n\n```python\ncustomers = customer_repository.match(criteria)\n```\n\n## Immutability\n\nAll classes are immutable frozen dataclasses. Methods return new instances:\n\n```python\nc1 = Criteria()\nc2 = c1.filter(\"name\", \"==\", \"Vincent\")\n\nassert c1 is not c2\nassert len(c1.filters) == 0\nassert len(c2.filters) == 1\n```\n\n## Migration from v0.x\n\nv1.0 introduces breaking changes:\n\n### Immutability\n\n```python\n# Old (v0.x) - mutation pattern\nc = Criteria()\nc.filter(\"name\", \"==\", \"Vincent\")  # mutated c\n\n# New (v1.x) - immutable, must reassign\nc = Criteria()\nc = c.filter(\"name\", \"==\", \"Vincent\")  # returns new instance\n```\n\n### Filter Factory Names\n\n```python\n# Old\nFilter.eq(\"name\", \"v\")\nFilter.neq(\"name\", \"v\")\nFilter.gt(\"age\", 18)\nFilter.gte(\"age\", 18)\nFilter.lt(\"age\", 65)\nFilter.lte(\"age\", 65)\n\n# New (aligned with PHP version)\nFilter.equal(\"name\", \"v\")\nFilter.not_equal(\"name\", \"v\")\nFilter.greater_than(\"age\", 18)\nFilter.greater_or_equal_than(\"age\", 18)\nFilter.less_than(\"age\", 65)\nFilter.less_or_equal_than(\"age\", 65)\n```\n\n### Order.by is now tuple\n\n```python\n# Old\nOrder.desc([\"name\", \"age\"])\n\n# New\nOrder.desc((\"name\", \"age\"))\n```\n\n### Criteria operators only accept Criteria\n\n```python\n# Old - operators accepted Filter/FilterGroup\nc = Criteria() | Filter.equal(\"a\", 1)\n\n# New - use with_filter_group instead\nc = Criteria().with_filter_group(FilterGroup.create(Filter.equal(\"a\", 1)))\n\n# Operators only work between Criteria objects\nc1 = Criteria().with_filter_group(group1)\nc2 = Criteria().with_filter_group(group2)\ncombined = c1 | c2  # OK\nmerged = c1 \u0026 c2    # OK\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcomplexheart%2Fpy-criteria","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcomplexheart%2Fpy-criteria","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcomplexheart%2Fpy-criteria/lists"}