From d9f6a4def42b663b8ee49fd533540f8535905a72 Mon Sep 17 00:00:00 2001 From: oasaph Date: Tue, 3 Jun 2025 17:12:16 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20feat(docs):=20add=20comprehensiv?= =?UTF-8?q?e=20documentation=20and=20instructions=20for=20LLM=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/instructions/INSTRUCTIONS_CODE.md | 63 +++++++++ .github/instructions/INSTRUCTIONS_COMMITS.md | 19 +++ .github/instructions/INSTRUCTIONS_DOCS.md | 30 +++++ .github/instructions/INSTRUCTIONS_PR_BODY.md | 43 ++++++ .vscode/settings.json | 16 ++- README.md | 2 +- python_flaggle/__init__.py | 11 ++ python_flaggle/flag.py | 131 +++++++++++++++++-- python_flaggle/flaggle.py | 102 ++++++++++++++- 9 files changed, 401 insertions(+), 16 deletions(-) create mode 100644 .github/instructions/INSTRUCTIONS_CODE.md create mode 100644 .github/instructions/INSTRUCTIONS_COMMITS.md create mode 100644 .github/instructions/INSTRUCTIONS_DOCS.md create mode 100644 .github/instructions/INSTRUCTIONS_PR_BODY.md diff --git a/.github/instructions/INSTRUCTIONS_CODE.md b/.github/instructions/INSTRUCTIONS_CODE.md new file mode 100644 index 0000000..bbb9b6d --- /dev/null +++ b/.github/instructions/INSTRUCTIONS_CODE.md @@ -0,0 +1,63 @@ +# Coding Instructions for LLM Tools + +## Purpose +Guidelines for LLMs (e.g., GitHub Copilot) to generate, edit, and review code in the Flaggle project. + +## Coding Standards +- Follow [PEP 8](https://peps.python.org/pep-0008/) for style. +- Use type annotations for all public functions and methods. +- Use Google-style docstrings for all public classes and methods. +- Prefer explicit, readable code over cleverness. +- Use triple double quotes (`"""`) for docstrings. +- Add examples in docstrings for public APIs. +- Use `from ... import ...` for imports within the package. + +## Docstring Directives +- **Short docstring**: Use for simple, self-explanatory functions or classes. One concise sentence, no blank line after the summary. +- **Long docstring**: Use for public, complex, or reusable code. Structure: + - First line: short summary. + - Blank line. + - Extended description (optional). + - Args: List all parameters, their types, and descriptions. + - Returns: Describe the return type and meaning. + - Raises: List exceptions that may be raised. + - Attributes: For classes, document all public attributes. + - Examples: (Optional) Add usage examples for clarity, using indented code blocks in docstrings and fenced code blocks (```python) in markdown/README. +- **Tone**: Use clear, concise, and neutral language. +- **Formatting**: Use triple double quotes (`"""`) and follow [PEP 257](https://peps.python.org/pep-0257/). +- **Compatibility**: This template is compatible with VS Code, PyCharm, Sphinx, and most Python docstring tools. Structure is inspired by Google and NumPy conventions. + +## File Structure +- Place new features in the appropriate module under `python_flaggle/`. +- Place all tests in `tests/`. + +## Testing +- All new code must be covered by tests. +- Use `pytest` for all tests. +- Strive for 100% code coverage. +- Test both typical and edge cases. + +## Example Docstring +```python +class MyClass: + """ + Short summary of the class. + + Attributes: + attr1 (type): Description. + """ + def my_method(self, param1: int) -> bool: + """ + Short summary of the method. + + Args: + param1 (int): Description. + Returns: + bool: Description. + Example: + ```python + obj = MyClass() + obj.my_method(1) + ``` + """ +``` diff --git a/.github/instructions/INSTRUCTIONS_COMMITS.md b/.github/instructions/INSTRUCTIONS_COMMITS.md new file mode 100644 index 0000000..fcb539d --- /dev/null +++ b/.github/instructions/INSTRUCTIONS_COMMITS.md @@ -0,0 +1,19 @@ +# Commit Message Instructions for LLM Tools + +## Purpose +Guidelines for LLMs to generate clear, conventional commit messages for the Flaggle project. + +## Format +- Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): + - `type(scope): subject` + - Example: `feat(flag): add support for float flag types` +- Types: feat, fix, docs, style, refactor, test, chore +- Use imperative mood (e.g., "add", not "adds") +- Limit subject line to 72 characters +- Use body to explain what and why, not how (if needed) +- Reference issues or PRs if relevant + +## Examples +- `fix(flaggle): handle ValueError in _fetch_flags` +- `docs(readme): clarify JSON schema for flags endpoint` +- `test(flag): add test for else branch in is_enabled` diff --git a/.github/instructions/INSTRUCTIONS_DOCS.md b/.github/instructions/INSTRUCTIONS_DOCS.md new file mode 100644 index 0000000..466430c --- /dev/null +++ b/.github/instructions/INSTRUCTIONS_DOCS.md @@ -0,0 +1,30 @@ +# Documentation Instructions for LLM Tools + +## Purpose +Guidelines for LLMs to generate and update documentation in the Flaggle project. + +## General Principles +- All public APIs must be documented in `README.md` and/or module docstrings. +- Use Markdown fenced code blocks (```python) for examples. +- Use clear, concise, and neutral language. +- Document the JSON schema for the flags endpoint. +- Update the Table of Contents if new sections are added. + +## README.md Structure +- Overview +- Features +- Installation +- Getting Started +- Flaggle Class: Configuration & Usage +- Supported Operations +- Advanced Usage: The Flag Class +- JSON Schema for Flags Endpoint +- Contributing +- License +- TODO / Roadmap + +## Best Practices +- Use headings and subheadings for clarity. +- Add usage examples for all major features. +- Keep documentation up to date with code changes. +- Add a changelog entry for significant documentation updates. diff --git a/.github/instructions/INSTRUCTIONS_PR_BODY.md b/.github/instructions/INSTRUCTIONS_PR_BODY.md new file mode 100644 index 0000000..82e074d --- /dev/null +++ b/.github/instructions/INSTRUCTIONS_PR_BODY.md @@ -0,0 +1,43 @@ +# Pull Request Body Instructions for LLM Tools + +## Purpose +Guidelines for LLMs to generate clear, informative PR bodies for the Flaggle project. + +## Structure +- **Summary**: Briefly describe what the PR does. +- **Motivation**: Why is this change needed? +- **Changes**: List major changes (new features, bug fixes, refactors, docs, tests). +- **Testing**: Describe how the change was tested. +- **Checklist**: + - [ ] Code follows style and docstring guidelines + - [ ] Tests added/updated and passing + - [ ] Documentation updated + - [ ] No unrelated changes +- **Related Issues/PRs**: Reference any related issues or pull requests. + +## Example +``` +## Summary +Add async support to Flaggle's flag fetching. + +## Motivation +Async support allows non-blocking flag updates in async applications. + +## Changes +- Add async versions of fetch/update methods +- Update tests for async +- Update README with async usage + +## Testing +- Added new async tests +- All tests pass locally + +## Checklist +- [x] Code follows style and docstring guidelines +- [x] Tests added/updated and passing +- [x] Documentation updated +- [x] No unrelated changes + +## Related Issues/PRs +Closes #42 +``` diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b38853..f49efd4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,19 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "github.copilot.chat.codeGeneration.enabled": true, + "github.copilot.chat.codeGeneration.instructions": [ + { + "file": ".github/instructions/INSTRUCTIONS_CODE.md", + }, + { + "file": ".github/instructions/INSTRUCTIONS_DOCS.md", + } + ], + "github.copilot.chat.commitMessageGeneration.instructions": [ + { + "file": ".github/instructions/INSTRUCTIONS_COMMITS.md", + } + ], } \ No newline at end of file diff --git a/README.md b/README.md index 71bc5b6..5dfe370 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,6 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file - [ ] **Async Support:** Add async/await support for non-blocking flag fetching and updates. - [ ] **Type Annotations & Validation:** Improve type safety and validation for flag values and operations. - [ ] **Better Error Handling & Logging:** More granular error reporting and logging options. -- [ ] **Extensive Documentation & Examples:** Expand documentation with more real-world usage patterns and advanced scenarios. +- [x] **Extensive Documentation & Examples:** Expand documentation with more real-world usage patterns and advanced scenarios. Contributions and suggestions are welcome! Please open an issue or pull request if you have ideas for improvements. \ No newline at end of file diff --git a/python_flaggle/__init__.py b/python_flaggle/__init__.py index d10951d..2b6f215 100644 --- a/python_flaggle/__init__.py +++ b/python_flaggle/__init__.py @@ -1,3 +1,14 @@ +"""Flaggle: Python feature flag management library. + +This package provides the Flaggle class for managing feature flags, as well as the Flag, FlagType, and FlagOperation classes for defining and evaluating individual flags. + +Exports: + Flaggle: Main entry point for feature flag management. + Flag: Represents a single feature flag. + FlagType: Enum of supported flag value types. + FlagOperation: Enum of supported flag operations. +""" + from python_flaggle.flaggle import Flaggle from python_flaggle.flag import Flag, FlagOperation, FlagType diff --git a/python_flaggle/flag.py b/python_flaggle/flag.py index 2f95f31..30f5b13 100644 --- a/python_flaggle/flag.py +++ b/python_flaggle/flag.py @@ -1,3 +1,13 @@ +"""Flag and flag operation definitions for the Flaggle library. + +This module provides the Flag, FlagType, and FlagOperation classes for defining and evaluating feature flags. + +Classes: + FlagType: Enum representing supported flag value types. + FlagOperation: Enum of supported flag comparison operations. + Flag: Represents a single feature flag and its evaluation logic. +""" + from enum import Enum from logging import getLogger from traceback import format_exc @@ -7,6 +17,17 @@ class FlagType(Enum): + """Enumeration of supported flag value types. + + Attributes: + BOOLEAN (str): Boolean flag type. + STRING (str): String flag type. + INTEGER (str): Integer flag type. + FLOAT (str): Float flag type. + NULL (str): Null flag type. + ARRAY (str): Array/list flag type. + EMPTY (str): Empty string flag type. + """ BOOLEAN: str = "boolean" STRING: str = "string" INTEGER: str = "integer" @@ -15,11 +36,23 @@ class FlagType(Enum): ARRAY: str = "array" EMPTY: str = "" - def __str__(self): + def __str__(self) -> str: + """Return the string value of the flag type.""" return self.value @classmethod - def from_value(cls, value) -> "FlagType": + def from_value(cls, value: Any) -> "FlagType": + """Infer the FlagType from a Python value. + + Args: + value (Any): The value to infer the type from. + + Returns: + FlagType: The inferred flag type. + + Raises: + TypeError: If the value type is not supported. + """ if isinstance(value, bool): return cls.BOOLEAN elif isinstance(value, str): @@ -41,6 +74,20 @@ def from_value(cls, value) -> "FlagType": class FlagOperation(Enum): + """Enumeration of supported flag comparison operations. + + Each operation is a callable that takes two arguments and returns a boolean. + + Supported operations: + EQ: Equal to + NE: Not equal to + GT: Greater than + GE: Greater than or equal to + LT: Less than + LE: Less than or equal to + IN: Value is in a list/array + NI: Value is not in a list/array + """ # fmt: off EQ = lambda first, second: first == second # noqa: E731 NE = lambda first, second: first != second # noqa: E731 @@ -54,6 +101,17 @@ class FlagOperation(Enum): @classmethod def from_string(cls, operation: str) -> "FlagOperation": + """Get a FlagOperation from a string name (case-insensitive). + + Args: + operation (str): The operation name (e.g., 'eq', 'gt'). + + Returns: + FlagOperation: The corresponding operation. + + Raises: + ValueError: If the operation name is invalid. + """ operation = operation.upper() try: @@ -65,50 +123,90 @@ def from_string(cls, operation: str) -> "FlagOperation": class Flag: + """Represents a single feature flag and its evaluation logic. + + Attributes: + name (str): The unique name of the flag. + value (Any): The value of the flag (bool, str, int, float, list, or None). + description (Optional[str]): Optional human-readable description. + operation (Optional[FlagOperation]): Optional operation for evaluation. + flag_type (FlagType): The inferred type of the flag value. + """ def __init__( self, name: str, - value, + value: Any, description: Optional[str] = None, operation: Optional[FlagOperation] = None, ): + """Initialize a Flag instance. + + Args: + name (str): The unique name of the flag. + value (Any): The value of the flag. + description (Optional[str]): Optional description. + operation (Optional[FlagOperation]): Optional operation for evaluation. + """ self._name: str = name self._value = value self._description: Optional[str] = description self._operation: Optional[FlagOperation] = operation - self._flag_type: FlagType = FlagType.from_value(value=value) - def __str__(self): + def __str__(self) -> str: + """Return a string representation of the flag.""" return f'Flag(name="{self._name}", description="{self._description}", status="{self.status}")' def __eq__(self, other: "Flag") -> bool: + """Check equality with another Flag instance.""" if isinstance(other, Flag): return self._name == other._name and self._value == other._value return NotImplemented def __ne__(self, other: "Flag") -> bool: + """Check inequality with another Flag instance.""" return not self.__eq__(other) @property def name(self) -> str: + """The unique name of the flag.""" return self._name @property - def value(self) -> str: + def value(self) -> Any: + """The value of the flag.""" return self._value @property - def description(self) -> str: + def description(self) -> Optional[str]: + """The human-readable description of the flag, if any.""" return self._description @property - def status(self) -> str: + def status(self) -> bool: + """Whether the flag is considered enabled (truthy value). + + Returns: + bool: True if the flag is enabled, False otherwise. + """ if self._flag_type not in (FlagType.NULL, FlagType.EMPTY): return bool(self._value) return False def is_enabled(self, other_value: Optional[Any] = None) -> bool: + """Evaluate whether the flag is enabled, optionally comparing to another value. + + Args: + other_value (Optional[Any]): Value to compare against the flag's value (for non-boolean flags). + + Returns: + bool: True if the flag is enabled for the given value, False otherwise. + + Example: + >>> flag = Flag(name="min_version", value=3, operation=FlagOperation.GE) + >>> flag.is_enabled(4) + True + """ logger.debug(f"Flag {self._name} is of type {self._flag_type}") if self._flag_type == FlagType.BOOLEAN: return self._value @@ -129,6 +227,23 @@ def is_enabled(self, other_value: Optional[Any] = None) -> bool: @classmethod def from_json(cls: "Flag", data: dict) -> dict[str, "Flag"]: + """Create a dictionary of Flag objects from a JSON-like dictionary. + + Args: + data (dict): The JSON data containing a 'flags' key with a list of flag definitions. + + Returns: + dict[str, Flag]: A dictionary mapping flag names to Flag objects. + + Raises: + ValueError: If the JSON data is invalid or missing required fields. + + Example: + >>> json_data = {"flags": [{"name": "feature_x", "value": True}]} + >>> flags = Flag.from_json(json_data) + >>> flags["feature_x"].is_enabled() + True + """ try: flags_data = data.get("flags") if flags_data is None or not isinstance(flags_data, list): diff --git a/python_flaggle/flaggle.py b/python_flaggle/flaggle.py index 98e9d23..8113d3a 100644 --- a/python_flaggle/flaggle.py +++ b/python_flaggle/flaggle.py @@ -1,3 +1,12 @@ +"""Flaggle: Feature flag management for Python applications. + +This module provides the Flaggle class, which fetches and manages feature flags +from a remote JSON endpoint, enabling dynamic feature toggling and gradual rollouts. + +Classes: + Flaggle: Main class for fetching, updating, and evaluating feature flags. +""" + from datetime import datetime, timedelta, timezone from logging import getLogger from sched import scheduler @@ -13,6 +22,29 @@ class Flaggle: + """ + Main class for managing and evaluating feature flags in Python applications. + + Periodically fetches flag definitions from a remote JSON endpoint and provides + a simple API for evaluating those flags at runtime. + + Attributes: + _url (str): The endpoint URL to fetch flags from. + _interval (int): Polling interval in seconds. + _timeout (int): HTTP request timeout in seconds. + _verify_ssl (bool): Whether to verify SSL certificates. + _flags (dict): Dictionary of flag name to Flag object. + _last_update (datetime): Last time the flags were updated. + _scheduler (scheduler): Scheduler for periodic updates. + _scheduler_thread (Thread): Background thread for the scheduler. + + Example: + ```python + flaggle = Flaggle(url="https://api.example.com/flags", interval=60) + if flaggle.flags["feature_a"].is_enabled(): + print("Feature A is enabled!") + ``` + """ def __init__( self, url: str, @@ -21,6 +53,16 @@ def __init__( timeout: int = 10, verify_ssl: bool = True, ) -> None: + """ + Initialize a Flaggle instance. + + Args: + url (str): The HTTP(S) endpoint to fetch the flags JSON from. + interval (int): How often (in seconds) to poll for flag updates. + default_flags (dict, optional): Fallback flags if remote fetch fails. + timeout (int): HTTP request timeout in seconds (default: 10). + verify_ssl (bool): Whether to verify SSL certificates (default: True). + """ self._url: str = url self._interval: int = interval self._timeout: int = timeout @@ -36,35 +78,74 @@ def __init__( @property def flags(self) -> dict[str, list[dict[str, str]]]: - """Returns the current flags.""" + """ + Returns the current flags. + + Returns: + dict[str, list[dict[str, str]]]: Dictionary of flag name to Flag object. + """ return self._flags @property def last_update(self) -> datetime: - """Returns the last update time of the flags.""" + """ + Returns the last update time of the flags. + + Returns: + datetime: The last time the flags were updated. + """ return self._last_update @property def url(self) -> str: - """Returns the URL from which flags are fetched.""" + """ + Returns the URL from which flags are fetched. + + Returns: + str: The endpoint URL. + """ return self._url @property def interval(self) -> int: - """Returns the update interval in seconds.""" + """ + Returns the update interval in seconds. + + Returns: + int: Polling interval in seconds. + """ return self._interval @property def timeout(self) -> int: - """Returns the timeout for HTTP requests.""" + """ + Returns the timeout for HTTP requests. + + Returns: + int: HTTP request timeout in seconds. + """ return self._timeout @property def verify_ssl(self) -> bool: - """Returns whether SSL verification is enabled for HTTP requests.""" + """ + Returns whether SSL verification is enabled for HTTP requests. + + Returns: + bool: True if SSL verification is enabled, False otherwise. + """ return self._verify_ssl def _fetch_flags(self) -> dict[str, list[dict[str, str]]]: + """ + Fetch flags from the configured remote endpoint. + + Returns: + dict[str, list[dict[str, str]]]: Dictionary of flag name to Flag object. + Raises: + RequestException: If the HTTP request fails. + ValueError: If the response format is invalid. + """ try: logger.info("Fetching flags from %s", self._url) response = get(self._url, timeout=self._timeout, verify=self._verify_ssl) @@ -83,6 +164,9 @@ def _fetch_flags(self) -> dict[str, list[dict[str, str]]]: return {} def _update(self) -> None: + """ + Update the internal flag dictionary by fetching the latest flags. + """ flags_data = self._fetch_flags() if flags_data: self._flags = flags_data @@ -91,6 +175,9 @@ def _update(self) -> None: logger.debug("Current flags: %s", self._flags) def _schedule_update(self) -> None: + """ + Start the background scheduler for periodic flag updates. + """ def run_scheduler(): self._scheduler.enter(self._interval, 1, self.recurring_update) self._scheduler.run() @@ -99,5 +186,8 @@ def run_scheduler(): self._scheduler_thread.start() def recurring_update(self) -> None: + """ + Periodically update flags at the configured interval. + """ self._update() self._scheduler.enter(self._interval, 1, self.recurring_update)