{"id":19564453,"url":"https://github.com/u8slvn/sutoppu","last_synced_at":"2025-04-09T16:05:33.634Z","repository":{"id":46283638,"uuid":"160944583","full_name":"u8slvn/sutoppu","owner":"u8slvn","description":"A simple python implementation of Specification pattern.","archived":false,"fork":false,"pushed_at":"2025-03-31T21:19:24.000Z","size":88,"stargazers_count":37,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T16:05:27.274Z","etag":null,"topics":["business-rules","ddd","python","specification","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/u8slvn.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-12-08T13:59:55.000Z","updated_at":"2025-03-26T01:12:00.000Z","dependencies_parsed_at":"2024-03-08T11:03:36.269Z","dependency_job_id":null,"html_url":"https://github.com/u8slvn/sutoppu","commit_stats":{"total_commits":36,"total_committers":1,"mean_commits":36.0,"dds":0.0,"last_synced_commit":"c8c227dcf3622031199eef1259418a8343be749c"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/u8slvn%2Fsutoppu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/u8slvn%2Fsutoppu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/u8slvn%2Fsutoppu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/u8slvn%2Fsutoppu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/u8slvn","download_url":"https://codeload.github.com/u8slvn/sutoppu/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248065288,"owners_count":21041871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["business-rules","ddd","python","specification","specification-pattern"],"created_at":"2024-11-11T05:22:06.523Z","updated_at":"2025-04-09T16:05:33.616Z","avatar_url":"https://github.com/u8slvn.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sutoppu\n\n[![Pypi Version](https://img.shields.io/pypi/v/sutoppu.svg)](https://pypi.org/project/sutoppu/)\n[![Python Version](https://img.shields.io/pypi/pyversions/sutoppu)](https://pypi.org/project/sutoppu/)\n[![CI](https://github.com/u8slvn/sutoppu/actions/workflows/ci.yml/badge.svg)](https://github.com/u8slvn/sutoppu/actions/workflows/ci.yml)\n[![Coverage Status](https://coveralls.io/repos/github/u8slvn/sutoppu/badge.svg?branch=master)](https://coveralls.io/github/u8slvn/sutoppu?branch=master)\n[![Project license](https://img.shields.io/pypi/l/sutoppu)](https://pypi.org/project/sutoppu/)\n[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\n**Sutoppu** (ストップ - Japanese from English *Stop*) is a lightweight implementation of the Specification pattern for Python, enabling elegant business rule composition through boolean logic.\n\n## Table of Contents\n\n- [Introduction](#introduction)\n- [Installation](#installation)\n- [Core Concepts](#core-concepts)\n- [Basic Usage](#basic-usage)\n- [Advanced Features](#advanced-features)\n  - [Combining Specifications](#combining-specifications)\n  - [Call Syntax](#call-syntax)\n  - [Error Reporting](#error-reporting)\n- [Real-World Examples](#real-world-examples)\n- [API Reference](#api-reference)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Introduction\n\nThe Specification pattern is a powerful approach for encapsulating business rules in reusable, combinable objects. This pattern is especially valuable in domain-driven design and applications with complex validation logic.\n\nSutoppu brings this pattern to Python with a clean, intuitive API that leverages Python's operator overloading to create natural boolean expressions for your business rules.\n\n\u003e \"In computer programming, the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic. The pattern is frequently used in the context of domain-driven design.\" – [Wikipedia](https://en.wikipedia.org/wiki/Specification_pattern)\n\nSee [original paper](https://www.martinfowler.com/apsupp/spec.pdf) by Eric Evans and Martin Fowler for more information.\n\n## Installation\n\n```sh\npip install sutoppu\n```\n\nSutoppu is compatible with Python 3.8+ and has no external dependencies for Python 3.11+. For Python 3.8-3.10, it requires only the lightweight typing-extensions package.\n\n## Core Concepts\n\nThe foundation of Sutoppu is the `Specification` abstract base class, which:\n\n1. Defines a contract for checking if an object satisfies a specific rule\n2. Provides operators for combining specifications using boolean logic\n3. Includes built-in error tracking to identify which rules aren't satisfied\n\nEach specification must implement the `is_satisfied_by(candidate)` method, which returns `True` if the candidate meets the specification's criteria or `False` otherwise.\n\n## Basic Usage\n\nHere's a simple example demonstrating how to create and use specifications:\n\n```python\nfrom sutoppu import Specification\n\n\n# Define a domain entity\nclass User:\n    def __init__(self, username: str, email: str, age: int):\n        self.username = username\n        self.email = email\n        self.age = age\n\n\n# Create specifications for user validation\nclass ValidUsername(Specification[User]):\n    description = \"Username must be between 3 and 20 characters.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return 3 \u003c= len(user.username) \u003c= 20\n\n\nclass ValidEmail(Specification[User]):\n    description = \"Email must contain @ symbol.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return \"@\" in user.email\n\n\nclass AdultUser(Specification[User]):\n    description = \"User must be 18 or older.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return user.age \u003e= 18\n\n\n# Use the specifications\nuser1 = User(\"john_doe\", \"john@example.com\", 25)\nuser2 = User(\"jo\", \"invalid-email\", 17)\n\n# Combine specifications\nvalid_user = ValidUsername() \u0026 ValidEmail() \u0026 AdultUser()\n\n# Check if users are valid\nprint(valid_user.is_satisfied_by(user1))  # True\nprint(valid_user.is_satisfied_by(user2))  # False\n\n# Check which rules failed\nvalid_user.is_satisfied_by(user2)\nprint(valid_user.errors)\n\n# {\n#    'ValidUsername': 'Username must be between 3 and 20 characters.',\n#    'ValidEmail': 'Email must contain @ symbol.',\n#    'AdultUser': 'User must be 18 or older.'\n# }\n```\n\n## Advanced Features\n\n### Combining Specifications\n\nSutoppu overloads Python's bitwise operators to create a natural, expressive syntax for combining specifications:\n\n- `\u0026` (AND): Both specifications must be satisfied\n- `|` (OR): At least one specification must be satisfied\n- `~` (NOT): The specification must not be satisfied\n\nThese operators can be chained to create complex rule compositions:\n\n```python\n# User must be an adult with valid credentials, OR an approved minor\nvalid_account = (ValidUsername() \u0026 ValidEmail() \u0026 AdultUser()) | ApprovedMinor()\n\n# User must have valid credentials but must NOT be blacklisted\nactive_account = (ValidUsername() \u0026 ValidEmail()) \u0026 ~Blacklisted()\n```\n\n### Call Syntax\n\nFor a more concise syntax, specifications can be called directly as functions:\n\n```python\nadult_user = AdultUser()\n\n# These are equivalent:\nresult1 = adult_user.is_satisfied_by(user)\nresult2 = adult_user(user)\n```\n\n### Error Reporting\n\nSutoppu automatically tracks which specifications fail during validation. After checking a candidate, the `errors` dictionary provides detailed feedback on each failed rule:\n\n```python\ncomplex_spec = SpecA() \u0026 (SpecB() | SpecC()) \u0026 ~SpecD()\ncomplex_spec.is_satisfied_by(candidate)\n\nif complex_spec.errors:\n    for spec_name, description in complex_spec.errors.items():\n        print(f\"Failed rule: {spec_name} - {description}\")\n```\n\nKey features of error reporting:\n\n- The `errors` dictionary is reset before each validation\n- Keys are specification class names\n- Values are the descriptions defined in the specifications\n- Negated specifications that fail show \"Expected condition to NOT satisfy: [original description]\" as description\n\n## Real-World Examples\n\n### Product Eligibility for Promotion\n\n```python\nfrom sutoppu import Specification\nfrom datetime import datetime, timedelta\nfrom typing import Set, Literal\n\n\n# Define allowed category types for better type checking\nCategoryType = Literal[\"electronics\", \"home\", \"fashion\", \"books\", \"toys\", \"sports\"]\n\n\nclass Product:\n    def __init__(\n        self,\n        sku: str,\n        category: CategoryType,\n        price: float,\n        created_at: datetime,\n        stock: int,\n    ) -\u003e None:\n        self.sku = sku\n        self.category = category\n        self.price = price\n        self.created_at = created_at\n        self.stock = stock\n\n\nclass InPromotionCategory(Specification[Product]):\n    description = \"Product must be in eligible promotion category.\"\n    PROMO_CATEGORIES: Set[CategoryType] = {\"electronics\", \"home\", \"fashion\"}\n\n    def is_satisfied_by(self, product: Product) -\u003e bool:\n        return product.category in self.PROMO_CATEGORIES\n\n\nclass PriceThreshold(Specification[Product]):\n    description = \"Product must cost at least $50.\"\n\n    def is_satisfied_by(self, product: Product) -\u003e bool:\n        return product.price \u003e= 50.0\n\n\nclass NewArrival(Specification[Product]):\n    description = \"Product must be added within the last 30 days.\"\n\n    def is_satisfied_by(self, product: Product) -\u003e bool:\n        days_since_added = (datetime.now() - product.created_at).days\n        return days_since_added \u003c= 30\n\n\nclass InStock(Specification[Product]):\n    description = \"Product must be in stock.\"\n\n    def is_satisfied_by(self, product: Product) -\u003e bool:\n        return product.stock \u003e 0\n\n\n# Combine specifications for promotion eligibility\npromotion_eligible = (\n    InPromotionCategory() \u0026\n    PriceThreshold() \u0026\n    (NewArrival() | ~InStock())  # New arrivals or out-of-stock products\n)\n\n# Example products\neligible_product = Product(\n    sku=\"ELEC123\",\n    category=\"electronics\",\n    price=199.99,\n    created_at=datetime.now() - timedelta(days=5),  # 5 days ago\n    stock=10\n)\n\nineligible_product = Product(\n    sku=\"BOOK789\",\n    category=\"books\",\n    price=14.99,\n    created_at=datetime.now() - timedelta(days=60),  # 60 days ago\n    stock=20\n)\n\n# Check eligibility for both products\nis_eligible = promotion_eligible.is_satisfied_by(eligible_product)\nprint(f\"Electronics product eligible for promotion: {is_eligible}\")\n\n# Electronics product eligible for promotion: True\n\nis_ineligible = promotion_eligible.is_satisfied_by(ineligible_product)\nprint(f\"Book eligible for promotion: {is_ineligible}\")\n\n# Book eligible for promotion: False\n\n# Display failure reasons for the ineligible product\nprint(\"Failure reasons:\", promotion_eligible.errors)\n\n# Failure reasons:: {\n#   'InPromotionCategory': 'Product must be in eligible promotion category.',\n#   'PriceThreshold': 'Product must cost at least $50.',\n#   'NewArrival': 'Product must be added within the last 30 days.',\n#   'InStock': 'Expected condition to NOT satisfy: Product must be in stock.'\n# }\n```\n\n### User Permission System\n\n```python\nfrom sutoppu import Specification\nfrom typing import List, Set, Literal, Union\n\n\n# Define domain types\nRoleType = Literal[\"admin\", \"user\", \"manager\", \"auditor\"]\nDepartmentType = Literal[\"IT\", \"HR\", \"Finance\", \"Marketing\", \"Operations\"]\n\n\nclass User:\n    def __init__(\n        self,\n        roles: Set[RoleType],\n        department: DepartmentType,\n        access_level: int,\n        two_factor_enabled: bool,\n    ) -\u003e None:\n        self.roles = roles\n        self.department = department\n        self.access_level = access_level\n        self.two_factor_enabled = two_factor_enabled\n\n\nclass AdminRole(Specification[User]):\n    description = \"User must have admin role.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return \"admin\" in user.roles\n\n\nclass ITDepartment(Specification[User]):\n    description = \"User must be in IT department.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return user.department == \"IT\"\n\n\nclass SeniorAccessLevel(Specification[User]):\n    description = \"User must have senior access level.\"\n    SENIOR_THRESHOLD: int = 7\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return user.access_level \u003e= self.SENIOR_THRESHOLD\n\n\nclass TwoFactorEnabled(Specification[User]):\n    description = \"User must have 2FA enabled.\"\n\n    def is_satisfied_by(self, user: User) -\u003e bool:\n        return user.two_factor_enabled\n\n\n# Define sensitive data access rule\ncan_access_sensitive_data = (\n    (AdminRole() | (ITDepartment() \u0026 SeniorAccessLevel())) \u0026\n    TwoFactorEnabled()\n)\n\n# Example check with a regular user\nregular_user = User(\n    roles={\"user\"},\n    department=\"Finance\",\n    access_level=6,\n    two_factor_enabled=True\n)\n\n# Check permission\nhas_access = can_access_sensitive_data.is_satisfied_by(regular_user)\nprint(f\"Regular user can access sensitive data: {has_access}\")\n\n# Regular user can access sensitive data: False\n\n# Check which rules failed\nprint(\"Failed rules:\", can_access_sensitive_data.errors)\n\n# Failed rules: {\n#   'AdminRole': 'User must have admin role.',\n#   'ITDepartment': 'User must be in IT department.',\n#   'SeniorAccessLevel': 'User must have senior access level.'\n# }\n```\n\n## API Reference\n\n### `Specification[T]`\n\nAbstract base class for creating specifications. Type parameter `T` defines the type of objects being checked.\n\n**Attributes:**\n\n- `description`: Class attribute for describing the rule (default: \"No description provided.\")\n- `errors`: Dictionary of failed specifications, with class names as keys and descriptions as values\n\n**Methods:**\n\n- `is_satisfied_by(candidate: T) -\u003e bool`: Abstract method that must be implemented by concrete specifications\n- `__and__(other: Specification[T]) -\u003e Specification[T]`: Combine with another specification using AND logic\n- `__or__(other: Specification[T]) -\u003e Specification[T]`: Combine with another specification using OR logic\n- `__invert__() -\u003e Specification[T]`: Negate the specification (NOT logic)\n- `__call__(candidate: T) -\u003e bool`: Shorthand for calling `is_satisfied_by()`\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch: `git checkout -b feature/my-feature`\n3. Commit your changes: `git commit -am 'Add my feature'`\n4. Push to the branch: `git push origin feature/my-feature`\n5. Submit a pull request\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](https://github.com/u8slvn/sutoppu/blob/master/LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fu8slvn%2Fsutoppu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fu8slvn%2Fsutoppu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fu8slvn%2Fsutoppu/lists"}