diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7b6b2ae --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..20c2285 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## What this changes + + + +## Checklist + +- [ ] `flake8 regexsolver tests`, `mypy regexsolver` and `pytest` pass +- [ ] New public items carry a docstring, and anything user-facing is in the README +- [ ] A test covers the change +- [ ] `CHANGELOG.md` is updated under `## [Unreleased]` +- [ ] The change runs on Python 3.10, the version `requires-python` declares, or the bump is intentional and noted +- [ ] `regexsolver/_generated/` is only touched by `./generate-api.sh`, once the change is live in the API's published specification diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dd3dee..102990b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,10 +11,10 @@ jobs: name: Lint & Type Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.12" cache: "pip" @@ -40,10 +40,10 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} cache: "pip" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3407c30..3b74a33 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.12" cache: "pip" @@ -33,10 +33,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v7 with: python-version: "3.12" @@ -47,7 +47,7 @@ jobs: run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-package-distributions path: dist/ @@ -64,7 +64,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7fc0667 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2026-08-08 + +The singleton `RegexSolver` becomes an instantiable `RegexSolverClient`, with `AsyncRegexSolverClient` beside it, every operation moves from `Term` onto the client, and the SDK covers the whole API rather than the seven endpoints it knew about. The API moved with it, to a new contract served under `/v1`, so 1.0.x no longer reaches an endpoint that exists; see *Compatibility* below. + +### Added + +- `RegexSolverClient(api_token, base_url=..., auto_batch=True, max_terms_per_request=None)` and `AsyncRegexSolverClient`, taking the same arguments. Several clients, each with its own token, can exist in one process; both work as context managers and release their session on `close()` / `aclose()`. The synchronous client drives the asynchronous one on a shared background event loop. +- The operations the API gained since 1.0.3: `complement`, `concat`, `determinize`, `repeat`, `get_cardinality`, `get_dot`, `get_length`, `is_empty`, `is_empty_string`, `is_total`, `is_deterministic` and `get_account_limits`. +- Keyword options: `response_format` (`REGEX`, `FAIR` or `ANY`), `deterministic` and `execution_timeout` on the operations that return a term; `execution_timeout` alone on analyze operations and `determinize`. +- `generate_strings(term, limit, offset, ...)`, which pages through the language instead of returning a fixed count from the start, taking `path_order`, `character_order`, `seed`, `min_length`, `max_length` and `charset`. Paging is only consistent over a deterministic FAIR, hence `determinize()` and `is_deterministic()`. +- `Term.matches(string)`, evaluated locally with `re` — anchored `\A(?:...)\Z`, compiled with `re.DOTALL` — rather than by the API. It raises on a FAIR whose pattern is not known yet, and returns `False` for the empty language, which the engine writes as `[]`. +- An exception hierarchy under `RegexSolverError`, so a caller can catch the case it handles instead of matching on a message. `ApiError` carries `status_code` and `body`, and splits per status down to `RegexSyntaxError`, `TimeoutExceededError`, `QuotaExceededError` and the rest. +- A rate limiter, shared by every client holding the same token: a 429 sets a deadline from `Retry-After`, requests wait for it, and the operation is retried within a five-minute budget. +- Automatic batching: `concat`, `intersection` and `union` split a call carrying more terms than the account allows per request and fold the results back into one, each constituent request counting against the monthly quota. Disable with `auto_batch=False`, or lower the split with `max_terms_per_request`. +- A per-term cache of what the API has already returned — cardinality, length, pattern, dot, and the empty, empty-string, total and deterministic flags — so asking twice costs one request. +- `Term.serialize()` / `Term.deserialize()`, round-tripping the `regex=` / `fair=` form, plus `__eq__`, `__hash__`, `get_value()`, `to_dto()` and `from_dto()`. +- `AccountLimits`, `Cardinality` (`Integer`, `BigInteger`, `Infinite`), `Length`, `ResponseFormat`, `PathOrder` and `CharacterOrder` as exported models. +- `generate-api.sh`, which regenerates `regexsolver/_generated/` from the specification the API publishes at `https://api.regexsolver.com/openapi.json`; `.openapi-generator-ignore` protects the hand-written files. +- CI running flake8 and mypy, then the tests on Python 3.10 through 3.14, on pushes to `main`, on pull requests, and before a release is published. +- A `CHANGELOG.md`, this file, a pull request template, and Dependabot updates. + +### Changed + +- The HTTP layer is an `asyncio` client generated from the OpenAPI specification, on `aiohttp`, instead of hand-written `requests` calls, with `regexsolver/` the hand-written surface over `regexsolver/_generated/`. +- `Term` is an abstract base class with `RegexTerm` and `FairTerm` subclasses rather than a pydantic model; `Term.regex()` and `Term.fair()` are unchanged. +- `Term.get_pattern()` and `Term.get_fair()` read the term's own value and its cache, returning `None` when the other format has not been resolved yet. Resolving a pattern is `client.get_pattern(term)`. +- The minimum supported Python version is 3.10, declared in `requires-python`, up from 3.7. +- The package is built from `pyproject.toml` alone, with the test dependencies under the `test` extra. +- The tests drive the clients with `unittest.mock` over the generated API instead of matching URLs with `requests_mock` and loading fixtures from `tests/assets/`. + +### Removed + +- `RegexSolver`, with `get_instance()` and `initialize()`. +- The operation methods on `Term`: `intersection()`, `union()`, `subtraction()`, `is_equivalent_to()`, `is_subset_of()`, `generate_strings()` and `get_details()`. +- `Details`, along with the `regexsolver.details` module, which returned cardinality, length and the empty and total flags in one response. +- `setup.py`, `requirements.txt` and `test-requirements.txt`. + +### Compatibility + +- `RegexSolver.get_instance().initialize(token)` becomes `RegexSolverClient(token)`, and a term method becomes a client method taking the terms as arguments: `term1.union(term2)` is `client.union(term1, term2)`, `is_equivalent_to` is `equivalent`, `is_subset_of` is `subset`, `subtraction` is `difference`, and `get_details` is `get_cardinality()`, `get_length()`, `is_empty()` and `is_total()`. +- `ApiError` no longer prefixes its message with `The API returned the following error: `; the message text differs and `status_code` is what to branch on. +- The endpoints moved from `https://api.regexsolver.com/api/*` to `/v1/*`, and `/api/analyze/details` is split into one `/v1/analyze/*` endpoint per property. 1.1.0 is the lowest version that works against the API. +- The serialized `regex=` / `fair=` form is unchanged, so a term persisted by 1.0.x deserializes. + +## [1.0.3] - 2024-08-11 + +### Fixed + +- `Length.minimum` is optional, so a response without one is parsed instead of raising a validation error. + +## [1.0.2] - 2024-08-10 + +### Fixed + +- `Cardinality.value` defaults to `None`, so an infinite cardinality, which carries no value, is parsed instead of raising a validation error. + +## [1.0.1] - 2024-08-09 + +### Changed + +- The README documents the full API. + +## [1.0.0] - 2024-08-01 + +Initial release. + +[1.1.0]: https://github.com/RegexSolver/regexsolver-python/compare/v1.0.3...v1.1.0 +[1.0.3]: https://github.com/RegexSolver/regexsolver-python/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/RegexSolver/regexsolver-python/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/RegexSolver/regexsolver-python/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/RegexSolver/regexsolver-python/releases/tag/v1.0.0 diff --git a/README.md b/README.md index fbdd91f..13b1f7c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # RegexSolver Python API Client + +[![PyPI](https://img.shields.io/pypi/v/regexsolver)](https://pypi.org/project/regexsolver/) +[![Python versions](https://img.shields.io/pypi/pyversions/regexsolver)](https://pypi.org/project/regexsolver/) +[![CI](https://github.com/RegexSolver/regexsolver-python/actions/workflows/ci.yml/badge.svg)](https://github.com/RegexSolver/regexsolver-python/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/pypi/l/regexsolver)](LICENSE) + [Homepage](https://regexsolver.com) | [Online Demo](https://regexsolver.com/demo) | [Documentation](https://docs.regexsolver.com) | [Developer Console](https://console.regexsolver.com) **RegexSolver** is a powerful toolkit for building, combining, and analyzing regular expressions. It is designed for constraint solvers, test generators, and other systems that need advanced regex operations. @@ -103,7 +109,7 @@ try: res = client.difference(term1, term2, execution_timeout=100) except TimeoutExceededError as error: - print(error) # The API returned the following error: The operation took too much time. + print(error) # The operation took too much time. ``` Timeout is best effort. The exact time is not guaranteed. @@ -151,6 +157,8 @@ Timeout is best effort. The exact time is not guaranteed. *Note: For `AsyncRegexSolverClient`, this method is a coroutine and must be awaited.* +See [CHANGELOG.md](CHANGELOG.md) for notable changes. + ## Cross-Language Support If you want to use this library with other programming languages, we provide: diff --git a/generate-api.sh b/generate-api.sh index e3831c0..d0abb23 100755 --- a/generate-api.sh +++ b/generate-api.sh @@ -1,12 +1,14 @@ #!/bin/bash -SPEC_FILE="../m-lab/shared/openapi.yaml" +# The API serves its own specification, which is the source the SDK is generated +# from. Pass a path or another URL as the first argument to generate against it. +SPEC="${1:-https://api.regexsolver.com/openapi.json}" OUT_DIR="./" PACKAGE_NAME="regexsolver._generated" echo "Running openapi-generator-cli..." openapi-generator-cli generate \ - -i "$SPEC_FILE" \ + -i "$SPEC" \ -g python \ -o "$OUT_DIR" \ --additional-properties=packageName="$PACKAGE_NAME",library=asyncio diff --git a/pyproject.toml b/pyproject.toml index c280ea7..8484dc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,8 @@ include = ["regexsolver*"] [tool.mypy] exclude = ["regexsolver/_generated/"] -# Generated code is not linted or type checked; it is regenerated from -# ../m-lab/shared/openapi.yaml by ./generate-api.sh. +# Generated code is not linted or type checked; it is regenerated from the +# specification the API publishes, by ./generate-api.sh. [[tool.mypy.overrides]] module = "regexsolver._generated.*" follow_imports = "silent"