From 72cae1cb92630aad54768b3c851c5947ad7cc8f0 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 28 Sep 2025 21:57:20 +0200 Subject: [PATCH 01/47] Implemetation of v1.1.0 --- pyproject.toml | 2 +- regexsolver/__init__.py | 366 ++++++++++++++---- regexsolver/details.py | 29 +- setup.py | 2 +- .../assets/response_analyze_cardinality.json | 4 + ...ils.json => response_analyze_details.json} | 2 +- ...on => response_analyze_details_empty.json} | 2 +- ...=> response_analyze_details_infinite.json} | 2 +- ...setOf.json => response_analyze_empty.json} | 0 .../assets/response_analyze_empty_string.json | 4 + ....json => response_analyze_equivalent.json} | 0 tests/assets/response_analyze_length.json | 5 + .../assets/response_analyze_length_empty.json | 3 + tests/assets/response_analyze_subset.json | 4 + tests/assets/response_analyze_total.json | 4 + tests/assets/response_compute_concat.json | 4 + ....json => response_compute_difference.json} | 0 ...son => response_compute_intersection.json} | 0 ...union.json => response_compute_union.json} | 0 ...gs.json => response_generate_strings.json} | 0 tests/serialization_test.py | 30 +- tests/term_operation_test.py | 210 +++++++--- 22 files changed, 525 insertions(+), 148 deletions(-) create mode 100644 tests/assets/response_analyze_cardinality.json rename tests/assets/{response_getDetails.json => response_analyze_details.json} (84%) rename tests/assets/{response_getDetails_empty.json => response_analyze_details_empty.json} (85%) rename tests/assets/{response_getDetails_infinite.json => response_analyze_details_infinite.json} (83%) rename tests/assets/{response_isSubsetOf.json => response_analyze_empty.json} (100%) create mode 100644 tests/assets/response_analyze_empty_string.json rename tests/assets/{response_isEquivalentTo.json => response_analyze_equivalent.json} (100%) create mode 100644 tests/assets/response_analyze_length.json create mode 100644 tests/assets/response_analyze_length_empty.json create mode 100644 tests/assets/response_analyze_subset.json create mode 100644 tests/assets/response_analyze_total.json create mode 100644 tests/assets/response_compute_concat.json rename tests/assets/{response_subtraction.json => response_compute_difference.json} (100%) rename tests/assets/{response_intersection.json => response_compute_intersection.json} (100%) rename tests/assets/{response_union.json => response_compute_union.json} (100%) rename tests/assets/{response_generateStrings.json => response_generate_strings.json} (100%) diff --git a/pyproject.toml b/pyproject.toml index bda1679..796bf24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "regexsolver" -version = "1.0.3" +version = "1.1.0" authors = [ { name = "RegexSolver", email = "contact@regexsolver.com" } ] diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index ae744d6..a107e23 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -1,12 +1,11 @@ +from enum import Enum from regexsolver.details import Details, Cardinality, Length from typing import List, Optional -from pydantic import BaseModel +from pydantic import Field, BaseModel import requests -from regexsolver.details import Details - class ApiError(Exception): """ @@ -28,7 +27,7 @@ def __init__(self): self.base_url = "https://api.regexsolver.com/" self.api_token = None self.headers = { - 'User-Agent': 'RegexSolver Python / 1.0.3', + 'User-Agent': 'RegexSolver Python / 1.1.0', 'Content-Type': 'application/json' } @@ -57,70 +56,115 @@ def _request(self, endpoint: str, request: BaseModel) -> dict: response = requests.post( self._get_request_url(endpoint), headers=self.headers, - json=request.model_dump() + json=request.model_dump(exclude_none=True) ) if response.ok: return response.json() - else: - raise ApiError(response.json().get('message')) - - def compute_intersection(self, request: 'MultiTermsRequest') -> 'Term': + try: + data = response.json() + msg = data.get("message", response.text) + except Exception: + msg = response.text + raise ApiError(msg) + + # Analyze + + def _analyze_details(self, term: 'Term') -> Details: + return Details(**self._request('api/analyze/details', term)) + + def _analyze_cardinality(self, term: 'Term') -> Cardinality: + return Cardinality(**self._request('api/analyze/cardinality', term)) + + def _analyze_length(self, term: 'Term') -> Length: + return Length(**self._request('api/analyze/length', term)) + + def _analyze_equivalent(self, request: 'MultiTermsRequest') -> bool: + return self._request('api/analyze/equivalent', request).get('value') + + def _analyze_subset(self, request: 'MultiTermsRequest') -> bool: + return self._request('api/analyze/subset', request).get('value') + + def _analyze_empty(self, term: 'Term') -> bool: + return self._request('api/analyze/empty', term).get('value') + + def _analyze_total(self, term: 'Term') -> bool: + return self._request('api/analyze/total', term).get('value') + + def _analyze_empty_string(self, term: 'Term') -> bool: + return self._request('api/analyze/empty_string', term).get('value') + + def _analyze_dot(self, term: 'Term') -> str: + return self._request('api/analyze/dot', term).get('value') + + # Compute + + def _compute_repeat(self, request: 'RepeatRequest') -> 'Term': + return Term(**self._request('api/compute/repeat', request)) + + def _compute_intersection(self, request: 'MultiTermsRequest') -> 'Term': return Term(**self._request('api/compute/intersection', request)) - def compute_union(self, request: 'MultiTermsRequest') -> 'Term': + def _compute_union(self, request: 'MultiTermsRequest') -> 'Term': return Term(**self._request('api/compute/union', request)) - def compute_subtraction(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('api/compute/subtraction', request)) - - def get_details(self, term: 'Term') -> Details: - return Details(**self._request('api/analyze/details', term)) - - def equivalence(self, request: 'MultiTermsRequest') -> bool: - return self._request('api/analyze/equivalence', request).get('value') + def _compute_difference(self, request: 'MultiTermsRequest') -> 'Term': + return Term(**self._request('api/compute/difference', request)) + + def _compute_concat(self, request: 'MultiTermsRequest') -> 'Term': + return Term(**self._request('api/compute/concat', request)) + + # Generate - def subset(self, request: 'MultiTermsRequest') -> bool: - return self._request('api/analyze/subset', request).get('value') - - def generate_strings(self, request: 'GenerateStringsRequest') -> List[str]: + def _generate_strings(self, request: 'GenerateStringsRequest') -> List[str]: return self._request('api/generate/strings', request).get('value') - - -_REGEX_PREFIX = "regex" -_FAIR_PREFIX = "fair" -_UNKNOWN_PREFIX = "unknown" + + +class TermType(str, Enum): + FAIR = "fair" + REGEX = "regex" class Term(BaseModel): """ - This class represents a term on which it is possible to perform operations. - It can either be a regular expression (regex) or a FAIR (Fast Automaton Internal Representation). + Represents a term on which operations can be performed. + A term can be either: + - A regular expression (`regex`) + - A FAIR (Fast Automaton Internal Representation, `fair`) + + Convenience constructors: + - `Term.regex(pattern: str)` + - `Term.fair(fair: str)` """ - type: str + type: TermType value: str _details: Optional['Details'] = None + _empty: Optional[bool] = None + _total: Optional[bool] = None + _empty_string: Optional[bool] = None + + model_config = {"use_enum_values": True} @classmethod def fair(cls, fair: str) -> 'Term': """ Initialize a Fast Automaton Internal Representation (FAIR). """ - return cls(type=_FAIR_PREFIX, value=fair) + return cls(type=TermType.FAIR, value=fair) @classmethod def regex(cls, pattern: str) -> 'Term': """ Initialize a regex. """ - return cls(type=_REGEX_PREFIX, value=pattern) + return cls(type=TermType.REGEX, value=pattern) def get_fair(self) -> Optional[str]: """ Return the Fast Automaton Internal Representation (FAIR). """ - if type == _FAIR_PREFIX: + if self.type == TermType.FAIR: return self.value return None @@ -128,88 +172,223 @@ def get_pattern(self) -> Optional[str]: """ Return the regular expression pattern. """ - if type == _REGEX_PREFIX: + if self.type == TermType.REGEX: return self.value return None def get_details(self) -> Details: """ - Get the details of this term. - Cache the result to avoid calling the API again if this method is called multiple times. + Analyze this term and return detailed information including cardinality, + length, and whether it is empty or total. + + Results are cached on the instance to avoid repeated API calls. """ if self._details: return self._details else: - self._details = RegexSolver.get_instance().get_details(self) + self._details = RegexSolver.get_instance()._analyze_details(self) return self._details - def generate_strings(self, count: int) -> List[str]: + def generate_strings(self, count: int, execution_timeout=None) -> List[str]: + """ + Generate up to `count` example strings that match this term. + + Parameters: + count: Maximum number of unique strings to generate. + execution_timeout: Timeout in milliseconds for the server. + + Returns: + A list of strings matched by this term. """ - Generate the given number of unique strings matched by this term. + request = GenerateStringsRequest(term=self, count=count, options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._generate_strings(request) + + def intersection(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - request = GenerateStringsRequest(term=self, count=count) - return RegexSolver.get_instance().generate_strings(request) + Compute the intersection of this term with one or more other terms. - def intersection(self, *terms: 'Term') -> 'Term': + Parameters: + terms: Additional terms to intersect with. + response_format: Output format (`regex`, `fair`, or `any`). + execution_timeout: Timeout in milliseconds for the server. + + Returns: + A new term representing the intersection. """ - Compute the intersection with the given terms and return the resulting term. + request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_intersection(request) + + def union(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - request = MultiTermsRequest(terms=[self] + list(terms)) - return RegexSolver.get_instance().compute_intersection(request) + Compute the union of this term with one or more other terms. - def union(self, *terms: 'Term') -> 'Term': + Parameters: + terms: Terms to combine with this one. + response_format: Output format (`regex`, `fair`, or `any`). + execution_timeout: Timeout in milliseconds for the server. + + Returns: + A new term representing the union. """ - Compute the union with the given terms and return the resulting term. + request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_union(request) + + def difference(self, term: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - request = MultiTermsRequest(terms=[self] + list(terms)) - return RegexSolver.get_instance().compute_union(request) + Compute the difference between this term and another. - def subtraction(self, term: 'Term') -> 'Term': + Parameters: + term: The term to subtract from this one. + response_format: Output format (`regex`, `fair`, or `any`). + execution_timeout: Timeout in milliseconds for the server. + + Returns: + A new term representing the set difference (this - term). """ - Compute the subtraction with the given term and return the resulting term. + request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_difference(request) + + def concat(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - request = MultiTermsRequest(terms=[self, term]) - return RegexSolver.get_instance().compute_subtraction(request) + Concatenate this term with one or more other terms. + + Parameters: + terms: Additional terms to append in sequence. + response_format: Output format (`regex`, `fair`, or `any`). + execution_timeout: Timeout in milliseconds for the server. - def is_equivalent_to(self, term: 'Term') -> bool: + Returns: + A new term representing the concatenation. """ - Check equivalence with the given term. + request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_concat(request) + + def equivalent(self, term: 'Term', execution_timeout=None) -> bool: """ - request = MultiTermsRequest(terms=[self, term]) - return RegexSolver.get_instance().equivalence(request) + Check whether this term is equivalent to another. - def is_subset_of(self, term: 'Term') -> bool: + Parameters: + term: The term to compare against. + execution_timeout: Timeout in milliseconds for the server. + + Returns: + True if both terms accept exactly the same language. """ - Check if is a subset of the given term. + request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._analyze_equivalent(request) + + def subset(self, term: 'Term', execution_timeout=None) -> bool: """ - request = MultiTermsRequest(terms=[self, term]) - return RegexSolver.get_instance().subset(request) + Check whether this term is a subset of another. - def serialize(self) -> str: + Parameters: + term: The term to compare against. + execution_timeout: Timeout in milliseconds for the server. + + Returns: + True if every string matched by this term is also matched by `term`. """ - Generate a string representation that can be parsed by deserialize(). + request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._analyze_subset(request) + + def is_empty(self) -> bool: """ - prefix = _UNKNOWN_PREFIX - if self.type == _FAIR_PREFIX: - prefix = _FAIR_PREFIX - elif self.type == _REGEX_PREFIX: - prefix = _REGEX_PREFIX + Check whether this term matches no string. - return prefix + "=" + self.value + Results are cached on the instance to avoid repeated API calls. + """ + if self._empty: + return self._empty + else: + self._empty = RegexSolver.get_instance()._analyze_empty(self) + return self._empty + + def is_total(self) -> bool: + """ + Check whether this term matches all possible strings. - def deserialize(string: str) -> Optional['Term']: + Results are cached on the instance to avoid repeated API calls. """ - Parse a string representation of a Term produced by serialize(). + if self._total: + return self._total + else: + self._total = RegexSolver.get_instance()._analyze_total(self) + return self._total + + def is_empty_string(self) -> bool: """ - if not string: - return None + Check whether this term matches only the empty string. + + Results are cached on the instance to avoid repeated API calls. + """ + if self._empty_string: + return self._empty_string + else: + self._empty_string = RegexSolver.get_instance()._analyze_empty_string(self) + return self._empty_string + + def get_dot(self) -> str: + """ + Get the GraphViz DOT representation of this term. + + Returns: + A DOT language string describing the automaton for this term. + """ + return RegexSolver.get_instance()._analyze_dot(self) + + def get_cardinality(self) -> Cardinality: + """ + Get the cardinality of this term. + + Returns: + A `Cardinality` object describing how many distinct strings + are matched. + """ + return RegexSolver.get_instance()._analyze_cardinality(self) + + def get_length(self) -> Length: + """ + Get the length bounds of this term. + + Returns: + A `Length` object with the minimum and maximum string length + matched by this term. + """ + return RegexSolver.get_instance()._analyze_length(self) - if string.startswith(_REGEX_PREFIX): - return Term.regex(string[len(_REGEX_PREFIX)+1:]) - elif string.startswith(_FAIR_PREFIX): - return Term.fair(string[len(_FAIR_PREFIX)+1:]) + def serialize(self) -> str: + """ + Return a string representation of this term in the format + `=`, which can later be parsed by `deserialize()`. + """ + if self.type == TermType.FAIR: + prefix = TermType.FAIR + elif self.type == TermType.REGEX: + prefix = TermType.REGEX else: + raise ValueError(f"Unknown type: {self.type}") + + return prefix + "=" + self.value + + @staticmethod + def deserialize(string: str) -> Optional['Term']: + """ + Parse a string representation produced by `serialize()`. + + Parameters: + string: The serialized term, e.g. `"regex=abc"`. + + Returns: + A Term instance, or None if the input is empty or invalid. + """ + if not string or "=" not in string: return None + prefix, value = string.split("=", 1) + if prefix == TermType.REGEX: + return Term.regex(value) + elif prefix == TermType.FAIR: + return Term.fair(value) + return None def __str__(self): return self.serialize() @@ -222,11 +401,42 @@ def __eq__(self, other): def __hash__(self): return hash(self.serialize()) - +class ResponseFormat(str, Enum): + ANY = "any" + REGEX = "regex" + FAIR = "fair" + +class ResponseOptions(BaseModel): + format: Optional[ResponseFormat] = None + + model_config = {"use_enum_values": True} + +class ExecutionOptions(BaseModel): + timeout: Optional[int] = None + +class RequestOptions(BaseModel): + schema_version: int = 1 + response: ResponseOptions = Field(default_factory=ResponseOptions) + execution: ExecutionOptions = Field(default_factory=ExecutionOptions) + + @classmethod + def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None): + return cls( + response=ResponseOptions(format=response_format), + execution=ExecutionOptions(timeout=execution_timeout), + ) + class MultiTermsRequest(BaseModel): terms: List[Term] + options: Optional[RequestOptions] = None +class RepeatRequest(BaseModel): + term: Term + min: int + max: Optional[int] + options: Optional[RequestOptions] = None class GenerateStringsRequest(BaseModel): term: Term count: int + options: Optional[RequestOptions] = None \ No newline at end of file diff --git a/regexsolver/details.py b/regexsolver/details.py index ec799c1..f7741d0 100644 --- a/regexsolver/details.py +++ b/regexsolver/details.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel, model_validator @@ -12,19 +12,16 @@ class Cardinality(BaseModel): def is_infinite(self) -> bool: """ - True if it has a finite number of values, False otherwise. + True if it has a infinite number of values, False otherwise. """ - if self.type == 'Infinite': - return True - else: - return False + return self.type == 'infinite' def __str__(self): - if self.type == 'Infinite': + if self.type == 'infinite': return "Infinite" - elif self.type == 'BigInteger': + elif self.type == 'bigInteger': return 'BigInteger' - elif self.type == 'Integer': + elif self.type == 'integer': return "Integer({})".format(self.value) else: return 'Unknown' @@ -39,10 +36,16 @@ class Length(BaseModel): maximum: Optional[int] @model_validator(mode="before") - def from_list(cls, values: list): - if len(values) != 2: - raise ValueError("List must contain exactly two elements") - return {'minimum': values[0], 'maximum': values[1]} + def from_list(cls, values: Any): + if isinstance(values, dict): + return {'minimum': values.get('min'), 'maximum': values.get('max')} + + if isinstance(values, list): + if len(values) != 2: + raise ValueError("List must contain exactly two elements") + return {'minimum': values[0], 'maximum': values[1]} + + return values def __str__(self): return "Length[minimum={}, maximum={}]".format( diff --git a/setup.py b/setup.py index d6b4483..10a66e1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="regexsolver", - version="1.0.3", + version="1.1.0", description="RegexSolver allows you to manipulate regular expressions as sets, enabling operations such as intersection, union, and subtraction.", long_description=open('README.md').read(), long_description_content_type='text/markdown', diff --git a/tests/assets/response_analyze_cardinality.json b/tests/assets/response_analyze_cardinality.json new file mode 100644 index 0000000..157edb5 --- /dev/null +++ b/tests/assets/response_analyze_cardinality.json @@ -0,0 +1,4 @@ +{ + "type": "integer", + "value": 5 +} \ No newline at end of file diff --git a/tests/assets/response_getDetails.json b/tests/assets/response_analyze_details.json similarity index 84% rename from tests/assets/response_getDetails.json rename to tests/assets/response_analyze_details.json index 65e0539..07ce803 100644 --- a/tests/assets/response_getDetails.json +++ b/tests/assets/response_analyze_details.json @@ -1,7 +1,7 @@ { "type": "details", "cardinality": { - "type": "Integer", + "type": "integer", "value": 2 }, "length": [ diff --git a/tests/assets/response_getDetails_empty.json b/tests/assets/response_analyze_details_empty.json similarity index 85% rename from tests/assets/response_getDetails_empty.json rename to tests/assets/response_analyze_details_empty.json index d33b6f3..f50bf22 100644 --- a/tests/assets/response_getDetails_empty.json +++ b/tests/assets/response_analyze_details_empty.json @@ -1,7 +1,7 @@ { "type": "details", "cardinality": { - "type": "Integer", + "type": "integer", "value": 0 }, "length": [ diff --git a/tests/assets/response_getDetails_infinite.json b/tests/assets/response_analyze_details_infinite.json similarity index 83% rename from tests/assets/response_getDetails_infinite.json rename to tests/assets/response_analyze_details_infinite.json index ae72fc8..fe08178 100644 --- a/tests/assets/response_getDetails_infinite.json +++ b/tests/assets/response_analyze_details_infinite.json @@ -1,7 +1,7 @@ { "type": "details", "cardinality": { - "type": "Infinite" + "type": "infinite" }, "length": [ 0, diff --git a/tests/assets/response_isSubsetOf.json b/tests/assets/response_analyze_empty.json similarity index 100% rename from tests/assets/response_isSubsetOf.json rename to tests/assets/response_analyze_empty.json diff --git a/tests/assets/response_analyze_empty_string.json b/tests/assets/response_analyze_empty_string.json new file mode 100644 index 0000000..84ed493 --- /dev/null +++ b/tests/assets/response_analyze_empty_string.json @@ -0,0 +1,4 @@ +{ + "type": "boolean", + "value": true +} \ No newline at end of file diff --git a/tests/assets/response_isEquivalentTo.json b/tests/assets/response_analyze_equivalent.json similarity index 100% rename from tests/assets/response_isEquivalentTo.json rename to tests/assets/response_analyze_equivalent.json diff --git a/tests/assets/response_analyze_length.json b/tests/assets/response_analyze_length.json new file mode 100644 index 0000000..0109dd3 --- /dev/null +++ b/tests/assets/response_analyze_length.json @@ -0,0 +1,5 @@ +{ + "type": "length", + "min": 0, + "max": 3 +} \ No newline at end of file diff --git a/tests/assets/response_analyze_length_empty.json b/tests/assets/response_analyze_length_empty.json new file mode 100644 index 0000000..eb3a50f --- /dev/null +++ b/tests/assets/response_analyze_length_empty.json @@ -0,0 +1,3 @@ +{ + "type": "length" +} \ No newline at end of file diff --git a/tests/assets/response_analyze_subset.json b/tests/assets/response_analyze_subset.json new file mode 100644 index 0000000..84ed493 --- /dev/null +++ b/tests/assets/response_analyze_subset.json @@ -0,0 +1,4 @@ +{ + "type": "boolean", + "value": true +} \ No newline at end of file diff --git a/tests/assets/response_analyze_total.json b/tests/assets/response_analyze_total.json new file mode 100644 index 0000000..25147f3 --- /dev/null +++ b/tests/assets/response_analyze_total.json @@ -0,0 +1,4 @@ +{ + "type": "boolean", + "value": false +} \ No newline at end of file diff --git a/tests/assets/response_compute_concat.json b/tests/assets/response_compute_concat.json new file mode 100644 index 0000000..c316789 --- /dev/null +++ b/tests/assets/response_compute_concat.json @@ -0,0 +1,4 @@ +{ + "type": "regex", + "value": "abcde" +} \ No newline at end of file diff --git a/tests/assets/response_subtraction.json b/tests/assets/response_compute_difference.json similarity index 100% rename from tests/assets/response_subtraction.json rename to tests/assets/response_compute_difference.json diff --git a/tests/assets/response_intersection.json b/tests/assets/response_compute_intersection.json similarity index 100% rename from tests/assets/response_intersection.json rename to tests/assets/response_compute_intersection.json diff --git a/tests/assets/response_union.json b/tests/assets/response_compute_union.json similarity index 100% rename from tests/assets/response_union.json rename to tests/assets/response_compute_union.json diff --git a/tests/assets/response_generateStrings.json b/tests/assets/response_generate_strings.json similarity index 100% rename from tests/assets/response_generateStrings.json rename to tests/assets/response_generate_strings.json diff --git a/tests/serialization_test.py b/tests/serialization_test.py index 8682208..9840455 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -1,6 +1,6 @@ import unittest -from regexsolver import GenerateStringsRequest, MultiTermsRequest, Term +from regexsolver import GenerateStringsRequest, MultiTermsRequest, RequestOptions, ResponseFormat, Term class SerializationTest(unittest.TestCase): @@ -31,7 +31,31 @@ def test_serialize_requests(self): {"type": "regex", "value": "ghi"} ] }, - request.model_dump() + request.model_dump(exclude_none=True) + ) + + request = MultiTermsRequest( + terms=[Term.regex(r"abc"), Term.regex(r"def"), Term.regex(r"ghi")], + options=RequestOptions.from_args(response_format=ResponseFormat.FAIR, execution_timeout=400) + ) + self.assertEqual( + { + "terms": [ + {"type": "regex", "value": "abc"}, + {"type": "regex", "value": "def"}, + {"type": "regex", "value": "ghi"} + ], + "options": { + "schema_version": 1, + "response": { + "format": "fair" + }, + "execution": { + "timeout": 400 + } + } + }, + request.model_dump(exclude_none=True) ) request = GenerateStringsRequest( @@ -41,7 +65,7 @@ def test_serialize_requests(self): "term": {"type": "regex", "value": "(abc|de){2,3}"}, "count": 10 }, - request.model_dump() + request.model_dump(exclude_none=True) ) request = Term.regex(r"(abc|de){2,3}") diff --git a/tests/term_operation_test.py b/tests/term_operation_test.py index 94a680a..da997da 100644 --- a/tests/term_operation_test.py +++ b/tests/term_operation_test.py @@ -2,15 +2,32 @@ import requests_mock import unittest -from regexsolver import ApiError, RegexSolver, Term +from regexsolver import ApiError, RegexSolver, ResponseFormat, Term class TermsOperationTest(unittest.TestCase): def setUp(self): - RegexSolver.get_instance().initialize("TOKEN") + RegexSolver.initialize("TOKEN") + + def test_analyze_cardinality(self): + with open('tests/assets/response_analyze_cardinality.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/cardinality", + json=json_response, status_code=200 + ) + + term = Term.regex(r"[0-4]") + cardinality = term.get_cardinality() + + self.assertEqual( + "Integer(5)", + str(cardinality) + ) - def test_get_details(self): - with open('tests/assets/response_getDetails.json') as response: + def test_analyze_details(self): + with open('tests/assets/response_analyze_details.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( @@ -26,8 +43,8 @@ def test_get_details(self): str(details) ) - def test_get_details_infinite(self): - with open('tests/assets/response_getDetails_infinite.json') as response: + def test_analyze_details_infinite(self): + with open('tests/assets/response_analyze_details_infinite.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( @@ -43,8 +60,8 @@ def test_get_details_infinite(self): str(details) ) - def test_get_details_empty(self): - with open('tests/assets/response_getDetails_empty.json') as response: + def test_analyze_details_empty(self): + with open('tests/assets/response_analyze_details_empty.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( @@ -59,103 +76,198 @@ def test_get_details_empty(self): "Details[cardinality=Integer(0), length=Length[minimum=None, maximum=None], empty=True, total=False]", str(details) ) + + def test_analyze_empty_string(self): + with open('tests/assets/response_analyze_empty_string.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/empty_string", + json=json_response, status_code=200 + ) - def test_generate_strings(self): - with open('tests/assets/response_generateStrings.json') as response: + term = Term.regex(r"") + + result = term.is_empty_string() + + self.assertEqual(True, result) + + def test_analyze_empty(self): + with open('tests/assets/response_analyze_empty.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/generate/strings", + "https://api.regexsolver.com/api/analyze/empty", json=json_response, status_code=200 ) - term = Term.regex(r"(abc|de){2}") - strings = term.generate_strings(10) + term = Term.regex(r"[]") - self.assertEqual(4, len(strings)) + result = term.is_empty() - def test_intersection(self): - with open('tests/assets/response_intersection.json') as response: + self.assertEqual(True, result) + + def test_analyze_total(self): + with open('tests/assets/response_analyze_total.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/compute/intersection", + "https://api.regexsolver.com/api/analyze/total", json=json_response, status_code=200 ) - term1 = Term.regex(r"(abc|de){2}") - term2 = Term.regex(r"de.*") - term3 = Term.regex(r".*abc") + term = Term.regex(r"abc") - result = term1.intersection(term2, term3) + result = term.is_total() - self.assertEqual("regex=deabc", str(result)) + self.assertEqual(False, result) + + def test_analyze_equivalent(self): + with open('tests/assets/response_analyze_equivalent.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/equivalent", + json=json_response, status_code=200 + ) + + term1 = Term.regex(r"(abc|de)") + term2 = Term.fair( + "rgmsW[1g2LvP=Gr&V>sLc#w-!No&(oq@Sf>X).?lI3{uh{80qWEH[#0.pHq@B-9o[LpP-a#fYI+") + + result = term1.equivalent(term2) + + self.assertEqual(False, result) + + def test_analyze_length_empty(self): + with open('tests/assets/response_analyze_length_empty.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/length", + json=json_response, status_code=200 + ) + + term = Term.regex(r"[]") + length = term.get_length() - def test_union(self): - with open('tests/assets/response_union.json') as response: + self.assertEqual( + "Length[minimum=None, maximum=None]", + str(length) + ) + + def test_analyze_length(self): + with open('tests/assets/response_analyze_length.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/compute/union", + "https://api.regexsolver.com/api/analyze/length", + json=json_response, status_code=200 + ) + + term = Term.regex(r"(abc)?") + length = term.get_length() + + self.assertEqual( + "Length[minimum=0, maximum=3]", + str(length) + ) + + def test_analyze_subset(self): + with open('tests/assets/response_analyze_subset.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/subset", + json=json_response, status_code=200 + ) + + term1 = Term.regex(r"de") + term2 = Term.regex(r"(abc|de)") + + result = term1.subset(term2) + + self.assertEqual(True, result) + + def test_compute_concat(self): + with open('tests/assets/response_compute_concat.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/compute/concat", json=json_response, status_code=200 ) term1 = Term.regex(r"abc") term2 = Term.regex(r"de") - term3 = Term.regex(r"fghi") - - result = term1.union(term2, term3) - self.assertEqual("regex=(abc|de|fghi)", str(result)) + result = term1.concat(term2, response_format=ResponseFormat.REGEX) - def test_subtraction(self): - with open('tests/assets/response_subtraction.json') as response: + self.assertEqual("regex=abcde", str(result)) + + def test_compute_difference(self): + with open('tests/assets/response_compute_difference.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/compute/subtraction", + "https://api.regexsolver.com/api/compute/difference", json=json_response, status_code=200 ) term1 = Term.regex(r"(abc|de)") term2 = Term.regex(r"de") - result = term1.subtraction(term2) + result = term1.difference(term2, response_format=ResponseFormat.REGEX) self.assertEqual("regex=abc", str(result)) - - def test_is_equivalent_to(self): - with open('tests/assets/response_isEquivalentTo.json') as response: + + def test_compute_intersection(self): + with open('tests/assets/response_compute_intersection.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/analyze/equivalence", + "https://api.regexsolver.com/api/compute/intersection", json=json_response, status_code=200 ) - term1 = Term.regex(r"(abc|de)") - term2 = Term.fair( - "rgmsW[1g2LvP=Gr&V>sLc#w-!No&(oq@Sf>X).?lI3{uh{80qWEH[#0.pHq@B-9o[LpP-a#fYI+") + term1 = Term.regex(r"(abc|de){2}") + term2 = Term.regex(r"de.*") + term3 = Term.regex(r".*abc") - result = term1.is_equivalent_to(term2) + result = term1.intersection(term2, term3, response_format=ResponseFormat.REGEX) - self.assertEqual(False, result) + self.assertEqual("regex=deabc", str(result)) - def test_is_subset_of(self): - with open('tests/assets/response_isSubsetOf.json') as response: + def test_compute_union(self): + with open('tests/assets/response_compute_union.json') as response: json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/analyze/subset", + "https://api.regexsolver.com/api/compute/union", json=json_response, status_code=200 ) - term1 = Term.regex(r"de") - term2 = Term.regex(r"(abc|de)") + term1 = Term.regex(r"abc") + term2 = Term.regex(r"de") + term3 = Term.regex(r"fghi") - result = term1.is_subset_of(term2) + result = term1.union(term2, term3, response_format=ResponseFormat.REGEX) - self.assertEqual(True, result) + self.assertEqual("regex=(abc|de|fghi)", str(result)) + + def test_generate_strings(self): + with open('tests/assets/response_generate_strings.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/generate/strings", + json=json_response, status_code=200 + ) + + term = Term.regex(r"(abc|de){2}") + strings = term.generate_strings(10) + + self.assertEqual(4, len(strings)) def test_error_response(self): with open('tests/assets/response_error.json') as response: From a8dd099baaf91400cb116bd485a362bd57bf1ce6 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:51:12 +0200 Subject: [PATCH 02/47] update readme + add tests + caching --- README.md | 223 +++++++-------------- regexsolver/__init__.py | 49 ++++- tests/assets/response_analyze_dot.json | 4 + tests/assets/response_analyze_pattern.json | 4 + tests/term_operation_test.py | 34 ++++ 5 files changed, 164 insertions(+), 150 deletions(-) create mode 100644 tests/assets/response_analyze_dot.json create mode 100644 tests/assets/response_analyze_pattern.json diff --git a/README.md b/README.md index 7335535..ee6f636 100644 --- a/README.md +++ b/README.md @@ -11,192 +11,123 @@ they were sets. ```sh pip install --upgrade regexsolver ``` +Requirements: Python >= 3.7 -### Requirements +## Quick Start -- Python >=3.7 - -## Usage - -In order to use the library you need to generate an API Token on our [Developer Console](https://console.regexsolver.com/). +1. Create an API token in the [Developer Console](https://console.regexsolver.com/). +2. Initialize the client and start working with terms: ```python -from regexsolver import RegexSolver, Term +from regexsolver import RegexSolver, ResponseFormat, Term -RegexSolver.initialize("YOUR TOKEN HERE") +# Initialize with your API token +RegexSolver.initialize("YOUR_API_TOKEN") +# Create terms term1 = Term.regex(r"(abc|de|fg){2,}") term2 = Term.regex(r"de.*") term3 = Term.regex(r".*abc") -term4 = Term.regex(r".+(abc|de).+") +# Compute intersection and difference +result = term1.intersection(term2, term3, response_format="regex").difference( + Term.regex(r".+(abc|de).+"), response_format=ResponseFormat.REGEX +) -result = term1.intersection(term2, term3)\ - .subtraction(term4) - -print(result) +print(result) # regex=deabc ``` -## Features +## Key Concepts & Limitations -- [Intersection](#intersection) -- [Union](#union) -- [Subtraction / Difference](#subtraction--difference) -- [Equivalence](#equivalence) -- [Subset](#subset) -- [Details](#details) -- [Generate Strings](#generate-strings) +RegexSolver supports a subset of regular expressions that adhere to the principles of regular languages. Here are the key characteristics and limitations of the regular expressions supported by RegexSolver: +- **Anchored Expressions:** All regular expressions in RegexSolver are anchored. This means that the expressions are treated as if they start and end at the boundaries of the input text. For example, the expression `abc` will match the string "abc" but not "xabc" or "abcx". +- **Lookahead/Lookbehind:** RegexSolver does not support lookahead (`(?=...)`) or lookbehind (`(?<=...)`) assertions. Using them returns an error. +- **Pure Regular Expressions:** RegexSolver focuses on pure regular expressions as defined in regular language theory. This means features that extend beyond regular languages, such as backreferences (`\1`, `\2`, etc.), are not supported. Any use of backreference would return an error. +- **Greedy/Ungreedy Quantifiers:** The concept of ungreedy (`*?`, `+?`, `??`) quantifiers is not supported. All quantifiers are treated as greedy. For example, `a*` or `a*?` will match the longest possible sequence of "a"s. +- **Line Feed and Dot:** RegexSolver handles all characters the same way. The dot `.` matches any Unicode character including line feed (`\n`). +- **Empty Regular Expressions:** The empty language (matches no string) is represented by constructs like `[]` (empty character class). This is distinct from the empty string. -### Intersection +RegexSolver is based on the [regex-syntax](https://docs.rs/regex-syntax/0.8.5/regex_syntax/) library for parsing patterns. Unsupported features are parsed but ignored; they do not raise an error unless they affect semantics that cannot be represented (e.g., backreferences). This allows for some flexibility in writing regular expressions, but it is important to be aware of the unsupported features to avoid unexpected behavior. -#### Request +## Response formats -Compute the intersection of the provided terms and return the resulting term. +The API can handle terms in two formats: +- `regex`: a regular expression pattern +- `fair`: FAIR (Fast Automaton Internal Representation); a representation used internally by the RegexSolver engine. -The maximum number of terms is currently limited to 10. +For some operations returning a FAIR is cheaper for the engine. If you do not force a format, it will choose the most suitable one. To control the output, pass `response_format`: ```python -term1 = Term.regex(r"(abc|de){2}") -term2 = Term.regex(r"de.*") -term3 = Term.regex(r".*abc") - -result = term1.intersection(term2, term3) -print(result) -``` - -#### Response - -``` -regex=deabc -``` - -### Union - -Compute the union of the provided terms and return the resulting term. - -The maximum number of terms is currently limited to 10. - -#### Request - -```python -term1 = Term.regex(r"abc") -term2 = Term.regex(r"de") -term3 = Term.regex(r"fghi") - -result = term1.union(term2, term3) -print(result) -``` - -#### Response - -``` -regex=(abc|de|fghi) -``` - -### Subtraction / Difference - -Compute the first term minus the second and return the resulting term. - -#### Request - -```python -term1 = Term.regex(r"(abc|de)") -term2 = Term.regex(r"de") - -result = term1.subtraction(term2) -print(result) -``` +from regexsolver import RegexSolver, ResponseFormat, Term -#### Response +term = Term.regex(r"(ab|c){2}") +u = term.union(Term.regex(r"de"), response_format=ResponseFormat.REGEX) +print(u) # regex=((c|ab){2}|de) -``` -regex=abc +i = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.FAIR) +print(i) # fair=... ``` -### Equivalence +If the response format does not matter the argument `response_format` can be omitted or its value can be set to `ResponseFormat.ANY`. -Analyze if the two provided terms are equivalent. +## Bounding execution time -#### Request +Long computations can be bounded with `execution_timeout` (milliseconds). Most methods on Term accepts it: ```python -term1 = Term.regex(r"(abc|de)") -term2 = Term.regex(r"(abc|de)*") - -result = term1.is_equivalent_to(term2) -print(result) +# Limit the server-side compute time to 300 ms +res = Term.regex(r"(a|b){100}").intersection( + Term.regex(r"a+"), + execution_timeout=300 +) ``` +If time is exceeded, the API will return an error. Catch `ApiError` to handle it. -#### Response +## API Overview -``` -False -``` +The client exposes three main groups of operations: -### Subset +### Analyze -Analyze if the second term is a subset of the first. +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.get_details()` | `Details` | Return cardinality, length bounds, and if it is empty or total. | +| `t.get_cardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | +| `t.get_length()` | `Length` | Returns the minimum and maximum length of matched strings. | +| `t.is_empty()` | `bool` | `True` if the term matches no string. | +| `t.is_total()` | `bool` | `True` if the term matches all possible strings. | +| `t.is_empty_string()` | `bool` | `True` if the term matches only the empty string. | +| `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | +| `t.subset(term: Term)` | `bool` | `True` if every string matched by `t` is also matched by `term`. Supports `execution_timeout`. | +| `t.get_dot()` | `str` | Return a GraphViz DOT representation of the automaton for the term. | +| `t.get_pattern()` | `str` | Return a regular expression pattern for the term. | -#### Request +### Compute -```java -term1 = Term.regex(r"de") -term2 = Term.regex(r"(abc|de)") +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.concat(*terms: Term)` | `Term` | Concatenate `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `t.union(*terms: Term)` | `Term` | Compute the union of `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `t.intersection(*terms: Term)` | `Term` | Compute the intersection of `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `t.difference(term: Term)` | `Term` | Compute the difference `t - term`. Supports `response_format` and `execution_timeout`. | +| `t.repeat(min: int, max: Optional[int])` | `Term` | Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | -result = term1.is_subset_of(term2) -print(result) -``` +### Generate -#### Response +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.generate_strings(count: int)` | `List[str]` | Generate up to `count` unique example strings matched by `t`. Supports `execution_timeout`. | -``` -True -``` +## Cross-Language Support -### Details +If you want to use this library with other programming languages, we provide a wide range of wrappers: +- [regexsolver-java](https://github.com/RegexSolver/regexsolver-java) +- [regexsolver-js](https://github.com/RegexSolver/regexsolver-js) -Compute the details of the provided term. - -The computed details are: - -- **Cardinality:** the number of possible values. -- **Length:** the minimum and maximum length of possible values. -- **Empty:** true if is an empty set (does not contain any value), false otherwise. -- **Total:** true if is a total set (contains all values), false otherwise. - -#### Request - -```python -term = Term.regex(r"(abc|de)") - -details = term.get_details() -print(details) -``` +For more information about how to use the wrappers, you can refer to our [guide](https://docs.regexsolver.com/getting-started.html). -#### Response +If you want to run the engine yourself you can also take a look at [regexsolver](https://github.com/RegexSolver/regexsolver). -``` -Details[cardinality=Integer(2), length=Length[minimum=2, maximum=3], empty=false, total=false] -``` - -### Generate Strings - -Generate the given number of strings that can be matched by the provided term. - -The maximum number of strings to generate is currently limited to 200. - -#### Request - -```python -term = Term.regex(r"(abc|de){2}") - -strings = term.generate_strings(3) -print(strings) -``` - -#### Response - -``` -['deabc', 'abcde', 'dede'] -``` +## License +This project is licensed under the MIT License. diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index a107e23..31c9169 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -97,6 +97,9 @@ def _analyze_empty_string(self, term: 'Term') -> bool: def _analyze_dot(self, term: 'Term') -> str: return self._request('api/analyze/dot', term).get('value') + def _analyze_pattern(self, term: 'Term') -> str: + return self._request('api/analyze/pattern', term).get('value') + # Compute def _compute_repeat(self, request: 'RepeatRequest') -> 'Term': @@ -140,9 +143,13 @@ class Term(BaseModel): type: TermType value: str _details: Optional['Details'] = None + _cardinality: Optional[Cardinality] = None + _length: Optional[Length] = None _empty: Optional[bool] = None _total: Optional[bool] = None _empty_string: Optional[bool] = None + _dot: Optional[str] = None + _pattern: Optional[str] = None model_config = {"use_enum_values": True} @@ -171,10 +178,17 @@ def get_fair(self) -> Optional[str]: def get_pattern(self) -> Optional[str]: """ Return the regular expression pattern. + + If the term is not a regex the pattern will be resolved. + Results are cached on the instance to avoid repeated API calls. """ if self.type == TermType.REGEX: return self.value - return None + elif self._pattern: + return self._pattern + else: + self._pattern = RegexSolver.get_instance()._analyze_pattern(self) + return self._pattern def get_details(self) -> Details: """ @@ -299,6 +313,8 @@ def is_empty(self) -> bool: """ if self._empty: return self._empty + elif self._details: + return self._details.empty else: self._empty = RegexSolver.get_instance()._analyze_empty(self) return self._empty @@ -311,6 +327,8 @@ def is_total(self) -> bool: """ if self._total: return self._total + elif self._details: + return self._details.total else: self._total = RegexSolver.get_instance()._analyze_total(self) return self._total @@ -330,31 +348,54 @@ def is_empty_string(self) -> bool: def get_dot(self) -> str: """ Get the GraphViz DOT representation of this term. + + Results are cached on the instance to avoid repeated API calls. Returns: A DOT language string describing the automaton for this term. """ - return RegexSolver.get_instance()._analyze_dot(self) + if self._dot: + return self._dot + else: + self._dot = RegexSolver.get_instance()._analyze_dot(self) + return self._dot def get_cardinality(self) -> Cardinality: """ Get the cardinality of this term. + + Results are cached on the instance to avoid repeated API calls. Returns: A `Cardinality` object describing how many distinct strings are matched. """ - return RegexSolver.get_instance()._analyze_cardinality(self) + + if self._cardinality: + return self._cardinality + elif self._details: + return self._details.cardinality + else: + self._cardinality = RegexSolver.get_instance()._analyze_cardinality(self) + return self._cardinality def get_length(self) -> Length: """ Get the length bounds of this term. + + Results are cached on the instance to avoid repeated API calls. Returns: A `Length` object with the minimum and maximum string length matched by this term. """ - return RegexSolver.get_instance()._analyze_length(self) + if self._length: + return self._length + elif self._length: + return self._details.length + else: + self._length = RegexSolver.get_instance()._analyze_length(self) + return self._length def serialize(self) -> str: """ diff --git a/tests/assets/response_analyze_dot.json b/tests/assets/response_analyze_dot.json new file mode 100644 index 0000000..5b71397 --- /dev/null +++ b/tests/assets/response_analyze_dot.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "value": "digraph G { ... }" +} \ No newline at end of file diff --git a/tests/assets/response_analyze_pattern.json b/tests/assets/response_analyze_pattern.json new file mode 100644 index 0000000..42cb41f --- /dev/null +++ b/tests/assets/response_analyze_pattern.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "value": "abc.*" +} \ No newline at end of file diff --git a/tests/term_operation_test.py b/tests/term_operation_test.py index da997da..d814a9e 100644 --- a/tests/term_operation_test.py +++ b/tests/term_operation_test.py @@ -77,6 +77,23 @@ def test_analyze_details_empty(self): str(details) ) + def test_analyze_dot(self): + with open('tests/assets/response_analyze_dot.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/dot", + json=json_response, status_code=200 + ) + + term = Term.regex(r"(abc|de)") + dot = term.get_dot() + + self.assertEqual( + "digraph G { ... }", + str(dot) + ) + def test_analyze_empty_string(self): with open('tests/assets/response_analyze_empty_string.json') as response: json_response = json.load(response) @@ -172,6 +189,23 @@ def test_analyze_length(self): "Length[minimum=0, maximum=3]", str(length) ) + + def test_analyze_pattern(self): + with open('tests/assets/response_analyze_pattern.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/analyze/pattern", + json=json_response, status_code=200 + ) + + term = Term.regex(r"abc.*") + pattern = term.get_pattern() + + self.assertEqual( + "abc.*", + str(pattern) + ) def test_analyze_subset(self): with open('tests/assets/response_analyze_subset.json') as response: From 41a01b37d634f30de0aa9dec4f29487589737244 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:49:31 +0200 Subject: [PATCH 03/47] readme wip --- README.md | 54 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index ee6f636..cfef2b0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RegexSolver Python API Client [Homepage](https://regexsolver.com) | [Online Demo](https://regexsolver.com/demo) | [Documentation](https://docs.regexsolver.com) | [Developer Console](https://console.regexsolver.com) -This repository contains the source code of the Python library for [RegexSolver](https://regexsolver.com) API. +Python client for the RegexSolver API. RegexSolver is a powerful regular expression manipulation toolkit, that gives you the power to manipulate regex as if they were sets. @@ -19,7 +19,7 @@ Requirements: Python >= 3.7 2. Initialize the client and start working with terms: ```python -from regexsolver import RegexSolver, ResponseFormat, Term +from regexsolver import RegexSolver, Term # Initialize with your API token RegexSolver.initialize("YOUR_API_TOKEN") @@ -30,11 +30,11 @@ term2 = Term.regex(r"de.*") term3 = Term.regex(r".*abc") # Compute intersection and difference -result = term1.intersection(term2, term3, response_format="regex").difference( - Term.regex(r".+(abc|de).+"), response_format=ResponseFormat.REGEX +result = term1.intersection(term2, term3).difference( + Term.regex(r".+(abc|de).+") ) -print(result) # regex=deabc +print(result.get_pattern()) # de(fg)*abc ``` ## Key Concepts & Limitations @@ -47,41 +47,49 @@ RegexSolver supports a subset of regular expressions that adhere to the principl - **Line Feed and Dot:** RegexSolver handles all characters the same way. The dot `.` matches any Unicode character including line feed (`\n`). - **Empty Regular Expressions:** The empty language (matches no string) is represented by constructs like `[]` (empty character class). This is distinct from the empty string. -RegexSolver is based on the [regex-syntax](https://docs.rs/regex-syntax/0.8.5/regex_syntax/) library for parsing patterns. Unsupported features are parsed but ignored; they do not raise an error unless they affect semantics that cannot be represented (e.g., backreferences). This allows for some flexibility in writing regular expressions, but it is important to be aware of the unsupported features to avoid unexpected behavior. - ## Response formats The API can handle terms in two formats: - `regex`: a regular expression pattern -- `fair`: FAIR (Fast Automaton Internal Representation); a representation used internally by the RegexSolver engine. +- `fair`: FAIR (Fast Automaton Internal Representation); a representation used internally by the RegexSolver engine + +FAIR is a stable, versioned internal format intended for programmatic use. -For some operations returning a FAIR is cheaper for the engine. If you do not force a format, it will choose the most suitable one. To control the output, pass `response_format`: +For some operations, returning FAIR is cheaper. If you do not force a format, it will choose the most suitable one. To control the output, pass `response_format`: ```python from regexsolver import RegexSolver, ResponseFormat, Term -term = Term.regex(r"(ab|c){2}") -u = term.union(Term.regex(r"de"), response_format=ResponseFormat.REGEX) -print(u) # regex=((c|ab){2}|de) +term = Term.regex(r"abcde") +result = term.union(Term.regex(r"de"), response_format=ResponseFormat.REGEX) +print(result) # regex=(abc)?de -i = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.FAIR) -print(i) # fair=... +result = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.FAIR) +print(result) # fair=... ``` If the response format does not matter the argument `response_format` can be omitted or its value can be set to `ResponseFormat.ANY`. +Regardless of a term's internal format, call `get_pattern()` to obtain a regex string. + ## Bounding execution time -Long computations can be bounded with `execution_timeout` (milliseconds). Most methods on Term accepts it: +Set a server-side compute timeout in milliseconds with `execution_timeout`: ```python -# Limit the server-side compute time to 300 ms -res = Term.regex(r"(a|b){100}").intersection( - Term.regex(r"a+"), - execution_timeout=300 -) +from regexsolver import ApiError, RegexSolver, Term + +# Limit the server-side compute time to 5 ms +try: + res = Term.regex(r".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c").difference( + Term.regex(r".*abc.*"), + execution_timeout=5 + ) +except ApiError as error: + print(error) # The API returned the following error: The operation took too much time. ``` -If time is exceeded, the API will return an error. Catch `ApiError` to handle it. + +There is no guarantee that the exact time will be respected. ## API Overview @@ -120,13 +128,13 @@ The client exposes three main groups of operations: ## Cross-Language Support -If you want to use this library with other programming languages, we provide a wide range of wrappers: +If you want to use this library with other programming languages, we provide: - [regexsolver-java](https://github.com/RegexSolver/regexsolver-java) - [regexsolver-js](https://github.com/RegexSolver/regexsolver-js) For more information about how to use the wrappers, you can refer to our [guide](https://docs.regexsolver.com/getting-started.html). -If you want to run the engine yourself you can also take a look at [regexsolver](https://github.com/RegexSolver/regexsolver). +You can also take a look at [regexsolver](https://github.com/RegexSolver/regexsolver) which contains the source code of the engine. ## License From 96626c6450e942ad8cf676447b4f0befd02cfd18 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:49:30 +0200 Subject: [PATCH 04/47] Add dotenv --- regexsolver/__init__.py | 52 ++++++++++++++++++++++++++--------------- requirements.txt | 3 ++- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 31c9169..2bea193 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -1,11 +1,13 @@ from enum import Enum +from importlib import metadata +import os from regexsolver.details import Details, Cardinality, Length from typing import List, Optional -from pydantic import Field, BaseModel +from pydantic import BaseModel import requests - +from dotenv import load_dotenv class ApiError(Exception): """ @@ -24,12 +26,18 @@ def __init__(self): raise Exception("This class is a singleton.") else: RegexSolver._instance = self - self.base_url = "https://api.regexsolver.com/" - self.api_token = None - self.headers = { + + load_dotenv() + + self._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com") + self._api_token = os.environ.get("REGEXSOLVER_API_TOKEN") or None + + self._headers = { 'User-Agent': 'RegexSolver Python / 1.1.0', 'Content-Type': 'application/json' } + if self._api_token: + self._headers['Authorization'] = f'Bearer {self._api_token}' @classmethod def get_instance(cls): @@ -40,22 +48,22 @@ def get_instance(cls): @classmethod def initialize(cls, api_token: str, base_url: str = None): instance = cls.get_instance() - instance.api_token = api_token + instance._api_token = api_token if base_url: - instance.base_url = base_url + instance._base_url = base_url - instance.headers['Authorization'] = f'Bearer {instance.api_token}' + instance._headers['Authorization'] = f'Bearer {instance._api_token}' def _get_request_url(self, endpoint: str) -> str: - if self.base_url.endswith('/'): - return self.base_url + endpoint + if self._base_url.endswith('/'): + return self._base_url + endpoint else: - return self.base_url + '/' + endpoint + return self._base_url + '/' + endpoint def _request(self, endpoint: str, request: BaseModel) -> dict: response = requests.post( self._get_request_url(endpoint), - headers=self.headers, + headers=self._headers, json=request.model_dump(exclude_none=True) ) @@ -457,15 +465,21 @@ class ExecutionOptions(BaseModel): class RequestOptions(BaseModel): schema_version: int = 1 - response: ResponseOptions = Field(default_factory=ResponseOptions) - execution: ExecutionOptions = Field(default_factory=ExecutionOptions) + response: Optional[ResponseOptions] = None + execution: Optional[ExecutionOptions] = None @classmethod - def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None): - return cls( - response=ResponseOptions(format=response_format), - execution=ExecutionOptions(timeout=execution_timeout), - ) + def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None) -> "RequestOptions | None": + response = None + if response_format: + response=ResponseOptions(format=response_format) + execution = None + if execution_timeout: + execution=ExecutionOptions(timeout=execution_timeout) + if response or execution: + return cls(response=response, execution=execution) + else: + return None class MultiTermsRequest(BaseModel): terms: List[Term] diff --git a/requirements.txt b/requirements.txt index 63b3919..767ba20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests>=2.20.0 pydantic<=2.5.3, >2.4.0; python_version<"3.8" -pydantic>=2.6.0; python_version>="3.8" \ No newline at end of file +pydantic>=2.6.0; python_version>="3.8" +python-dotenv==1.1.1 \ No newline at end of file From 8cb340e203de18d2f9fc9d4bb504d4b8c2ade3bd Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:34:44 +0200 Subject: [PATCH 05/47] Env variables should be read in initialize --- README.md | 25 ++++++++++++------------- regexsolver/__init__.py | 20 +++++++++----------- requirements.txt | 3 +-- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index cfef2b0..6f15048 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Requirements: Python >= 3.7 ```python from regexsolver import RegexSolver, Term -# Initialize with your API token -RegexSolver.initialize("YOUR_API_TOKEN") +# Set REGEXSOLVER_API_TOKEN in your env and call initialize(), +# or pass the token directly: +RegexSolver.initialize() # or RegexSolver.initialize("YOUR_API_TOKEN") # Create terms term1 = Term.regex(r"(abc|de|fg){2,}") @@ -47,15 +48,13 @@ RegexSolver supports a subset of regular expressions that adhere to the principl - **Line Feed and Dot:** RegexSolver handles all characters the same way. The dot `.` matches any Unicode character including line feed (`\n`). - **Empty Regular Expressions:** The empty language (matches no string) is represented by constructs like `[]` (empty character class). This is distinct from the empty string. -## Response formats +## Response Formats The API can handle terms in two formats: - `regex`: a regular expression pattern -- `fair`: FAIR (Fast Automaton Internal Representation); a representation used internally by the RegexSolver engine +- `fair`: FAIR (Fast Automaton Internal Representation), a stable, versioned programmatic format -FAIR is a stable, versioned internal format intended for programmatic use. - -For some operations, returning FAIR is cheaper. If you do not force a format, it will choose the most suitable one. To control the output, pass `response_format`: +If you do not force a format, the server picks the most efficient one. Control it with `response_format`: ```python from regexsolver import RegexSolver, ResponseFormat, Term @@ -68,9 +67,9 @@ result = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.F print(result) # fair=... ``` -If the response format does not matter the argument `response_format` can be omitted or its value can be set to `ResponseFormat.ANY`. +If the format does not matter, omit `response_format` or set `ResponseFormat.ANY`. -Regardless of a term's internal format, call `get_pattern()` to obtain a regex string. +Regardless of internal format, use `get_pattern()` to obtain a regex string. ## Bounding execution time @@ -89,7 +88,7 @@ except ApiError as error: print(error) # The API returned the following error: The operation took too much time. ``` -There is no guarantee that the exact time will be respected. +Timeout is best effort. The exact time is not guaranteed. ## API Overview @@ -100,14 +99,14 @@ The client exposes three main groups of operations: | Method | Return | Description | | -------- | ------- | ------- | | `t.get_details()` | `Details` | Return cardinality, length bounds, and if it is empty or total. | -| `t.get_cardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | -| `t.get_length()` | `Length` | Returns the minimum and maximum length of matched strings. | +| `t.get_cardinality()` | `Cardinality` | Return the cardinality of the term (i.e., the number of possible matched strings). | +| `t.get_length()` | `Length` | Return the minimum and maximum length of matched strings. | | `t.is_empty()` | `bool` | `True` if the term matches no string. | | `t.is_total()` | `bool` | `True` if the term matches all possible strings. | | `t.is_empty_string()` | `bool` | `True` if the term matches only the empty string. | | `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | | `t.subset(term: Term)` | `bool` | `True` if every string matched by `t` is also matched by `term`. Supports `execution_timeout`. | -| `t.get_dot()` | `str` | Return a GraphViz DOT representation of the automaton for the term. | +| `t.get_dot()` | `str` | Return a Graphviz DOT representation of the automaton for the term. | | `t.get_pattern()` | `str` | Return a regular expression pattern for the term. | ### Compute diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 2bea193..71e95cd 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -7,7 +7,6 @@ from typing import List, Optional from pydantic import BaseModel import requests -from dotenv import load_dotenv class ApiError(Exception): """ @@ -26,18 +25,11 @@ def __init__(self): raise Exception("This class is a singleton.") else: RegexSolver._instance = self - - load_dotenv() - - self._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com") - self._api_token = os.environ.get("REGEXSOLVER_API_TOKEN") or None - + self._headers = { 'User-Agent': 'RegexSolver Python / 1.1.0', 'Content-Type': 'application/json' } - if self._api_token: - self._headers['Authorization'] = f'Bearer {self._api_token}' @classmethod def get_instance(cls): @@ -46,11 +38,17 @@ def get_instance(cls): return cls._instance @classmethod - def initialize(cls, api_token: str, base_url: str = None): + def initialize(cls, api_token: str = None, base_url: str = None): instance = cls.get_instance() - instance._api_token = api_token + if api_token: + instance._api_token = api_token + else: + instance._api_token = os.environ.get("REGEXSOLVER_API_TOKEN") or None + if base_url: instance._base_url = base_url + else: + instance._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com") instance._headers['Authorization'] = f'Bearer {instance._api_token}' diff --git a/requirements.txt b/requirements.txt index 767ba20..63b3919 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ requests>=2.20.0 pydantic<=2.5.3, >2.4.0; python_version<"3.8" -pydantic>=2.6.0; python_version>="3.8" -python-dotenv==1.1.1 \ No newline at end of file +pydantic>=2.6.0; python_version>="3.8" \ No newline at end of file From da3bdf78d6e522e0ec6718e54a650ec8d3878569 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:36:42 +0200 Subject: [PATCH 06/47] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f15048..255b9e4 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The client exposes three main groups of operations: | `t.union(*terms: Term)` | `Term` | Compute the union of `t` with the given terms. Supports `response_format` and `execution_timeout`. | | `t.intersection(*terms: Term)` | `Term` | Compute the intersection of `t` with the given terms. Supports `response_format` and `execution_timeout`. | | `t.difference(term: Term)` | `Term` | Compute the difference `t - term`. Supports `response_format` and `execution_timeout`. | -| `t.repeat(min: int, max: Optional[int])` | `Term` | Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | +| `t.repeat(min: int, max: Optional[int])` | `Term` | Compute the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | ### Generate From 360cbd40e124f841d549fe5b9a51626b72c0dc7e Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:59:03 +0200 Subject: [PATCH 07/47] Update readme --- README.md | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 255b9e4..da56ac3 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ print(result) # fair=... If the format does not matter, omit `response_format` or set `ResponseFormat.ANY`. -Regardless of internal format, use `get_pattern()` to obtain a regex string. +Regardless of internal format, call `get_pattern()` to obtain a regex string. ## Bounding execution time @@ -92,38 +92,51 @@ Timeout is best effort. The exact time is not guaranteed. ## API Overview -The client exposes three main groups of operations: +`Term` exposes the following methods. + +### Build +| Method | Return | Description | +| -------- | ------- | ------- | +| `Term.fair(fair: str)` | `Term` | Creates a term from FAIR. | +| `Term.regex(regex: str)` | `Term` | Creates a term from a regex pattern. | ### Analyze | Method | Return | Description | | -------- | ------- | ------- | -| `t.get_details()` | `Details` | Return cardinality, length bounds, and if it is empty or total. | -| `t.get_cardinality()` | `Cardinality` | Return the cardinality of the term (i.e., the number of possible matched strings). | -| `t.get_length()` | `Length` | Return the minimum and maximum length of matched strings. | +| `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | +| `t.get_cardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | +| `t.get_details()` | `Details` | Returns cardinality, length bounds, and if it is empty or total. | +| `t.get_dot()` | `str` | Returns a Graphviz DOT representation of the automaton for the term. | +| `t.get_fair()` | `str` | Returns the FAIR of the term if defined. | +| `t.get_length()` | `Length` | Returns the minimum and maximum length of matched strings. | +| `t.get_pattern()` | `str` | Returns a regular expression pattern for the term. | | `t.is_empty()` | `bool` | `True` if the term matches no string. | -| `t.is_total()` | `bool` | `True` if the term matches all possible strings. | | `t.is_empty_string()` | `bool` | `True` if the term matches only the empty string. | -| `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | +| `t.is_total()` | `bool` | `True` if the term matches all possible strings. | | `t.subset(term: Term)` | `bool` | `True` if every string matched by `t` is also matched by `term`. Supports `execution_timeout`. | -| `t.get_dot()` | `str` | Return a Graphviz DOT representation of the automaton for the term. | -| `t.get_pattern()` | `str` | Return a regular expression pattern for the term. | ### Compute | Method | Return | Description | | -------- | ------- | ------- | -| `t.concat(*terms: Term)` | `Term` | Concatenate `t` with the given terms. Supports `response_format` and `execution_timeout`. | -| `t.union(*terms: Term)` | `Term` | Compute the union of `t` with the given terms. Supports `response_format` and `execution_timeout`. | -| `t.intersection(*terms: Term)` | `Term` | Compute the intersection of `t` with the given terms. Supports `response_format` and `execution_timeout`. | -| `t.difference(term: Term)` | `Term` | Compute the difference `t - term`. Supports `response_format` and `execution_timeout`. | -| `t.repeat(min: int, max: Optional[int])` | `Term` | Compute the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | +| `t.concat(*terms: Term)` | `Term` | Concatenates `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `t.difference(term: Term)` | `Term` | Computes the difference `t - term`. Supports `response_format` and `execution_timeout`. | +| `t.intersection(*terms: Term)` | `Term` | Computes the intersection of `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `t.repeat(min: int, max: Optional[int])` | `Term` | Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | +| `t.union(*terms: Term)` | `Term` | Computes the union of `t` with the given terms. Supports `response_format` and `execution_timeout`. | ### Generate | Method | Return | Description | | -------- | ------- | ------- | -| `t.generate_strings(count: int)` | `List[str]` | Generate up to `count` unique example strings matched by `t`. Supports `execution_timeout`. | +| `t.generate_strings(count: int)` | `List[str]` | Generates up to `count` unique example strings matched by `t`. Supports `execution_timeout`. | + +### Other +| Method | Return | Description | +| -------- | ------- | ------- | +| `t.serialize()` | `str` | Returns a serialized form of `t`. | +| `Term.deserialize(string: str)` | `Term` | Returns a deserialized term. | ## Cross-Language Support From 6ad21e7dd530c80a8f0278257cbc4c12b2d090e3 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:02:08 +0200 Subject: [PATCH 08/47] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da56ac3..3091bb9 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ RegexSolver supports a subset of regular expressions that adhere to the principl The API can handle terms in two formats: - `regex`: a regular expression pattern -- `fair`: FAIR (Fast Automaton Internal Representation), a stable, versioned programmatic format +- `fair`: FAIR (Fast Automaton Internal Representation), a stable, versioned programmatic format used internally by the engine If you do not force a format, the server picks the most efficient one. Control it with `response_format`: @@ -67,7 +67,7 @@ result = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.F print(result) # fair=... ``` -If the format does not matter, omit `response_format` or set `ResponseFormat.ANY`. +If the format does not matter, omit `response_format` or set it to `ResponseFormat.ANY`. Regardless of internal format, call `get_pattern()` to obtain a regex string. @@ -136,7 +136,7 @@ Timeout is best effort. The exact time is not guaranteed. | Method | Return | Description | | -------- | ------- | ------- | | `t.serialize()` | `str` | Returns a serialized form of `t`. | -| `Term.deserialize(string: str)` | `Term` | Returns a deserialized term. | +| `Term.deserialize(string: str)` | `Term` | Returns a deserialized term from the given `string`. | ## Cross-Language Support From 0a7b773f108388f941f14fab231b1f2e918b0827 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:05:50 +0200 Subject: [PATCH 09/47] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3091bb9..b2a291c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Timeout is best effort. The exact time is not guaranteed. ### Build | Method | Return | Description | | -------- | ------- | ------- | -| `Term.fair(fair: str)` | `Term` | Creates a term from FAIR. | +| `Term.fair(fair: str)` | `Term` | Creates a term from a FAIR. | | `Term.regex(regex: str)` | `Term` | Creates a term from a regex pattern. | ### Analyze From 63ff2b641aed57f292521ae303971d468878ee96 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:19:54 +0200 Subject: [PATCH 10/47] update readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b2a291c..05e3b4d 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,9 @@ RegexSolver supports a subset of regular expressions that adhere to the principl The API can handle terms in two formats: - `regex`: a regular expression pattern -- `fair`: FAIR (Fast Automaton Internal Representation), a stable, versioned programmatic format used internally by the engine +- `fair`: FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine -If you do not force a format, the server picks the most efficient one. Control it with `response_format`: +By default, the server returns whatever the operation produces, with no extra convertion. Override with `response_format`: ```python from regexsolver import RegexSolver, ResponseFormat, Term @@ -69,7 +69,7 @@ print(result) # fair=... If the format does not matter, omit `response_format` or set it to `ResponseFormat.ANY`. -Regardless of internal format, call `get_pattern()` to obtain a regex string. +Regardless of internal format, you can call `get_pattern()` to obtain a regex string. ## Bounding execution time From 58a8146238721cfbb64b417c510d36e191118b84 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:21:23 +0200 Subject: [PATCH 11/47] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05e3b4d..727d68b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ The API can handle terms in two formats: - `regex`: a regular expression pattern - `fair`: FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine -By default, the server returns whatever the operation produces, with no extra convertion. Override with `response_format`: +By default, the engine returns whatever the operation produces, with no extra convertion. Override with `response_format`: ```python from regexsolver import RegexSolver, ResponseFormat, Term From 8415d288e0b97bd5952453eb6ba92db75fc479f8 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:23:35 +0200 Subject: [PATCH 12/47] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 727d68b..87a80f9 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ print(result) # fair=... If the format does not matter, omit `response_format` or set it to `ResponseFormat.ANY`. -Regardless of internal format, you can call `get_pattern()` to obtain a regex string. +Regardless of internal format, you can always call `get_pattern()` to obtain the regex pattern of a term. ## Bounding execution time From b4e45141f5e64fb57339683b3e4784eb2cb321ea Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:24:50 +0200 Subject: [PATCH 13/47] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87a80f9..0e128ae 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ print(result) # fair=... If the format does not matter, omit `response_format` or set it to `ResponseFormat.ANY`. -Regardless of internal format, you can always call `get_pattern()` to obtain the regex pattern of a term. +Regardless of the format, you can always call `get_pattern()` to obtain the regex pattern of a term. ## Bounding execution time From bee91137bfb8fe8d95187c82e97da148122d2321 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:55:42 +0200 Subject: [PATCH 14/47] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 0e128ae..71457be 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # RegexSolver Python API Client [Homepage](https://regexsolver.com) | [Online Demo](https://regexsolver.com/demo) | [Documentation](https://docs.regexsolver.com) | [Developer Console](https://console.regexsolver.com) -Python client for the RegexSolver API. - -RegexSolver is a powerful regular expression manipulation toolkit, that gives you the power to manipulate regex as if -they were sets. +**RegexSolver** is a powerful regular expression manipulation toolkit that lets you manipulate regular expressions as if they were sets. It provides a powerful API to perform operations like union, intersection, and difference on regex patterns, enabling advanced regex analysis and transformation. ## Installation From b8fa9946df54683680f053aa894aa8bb1a750aab Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:08:21 +0200 Subject: [PATCH 15/47] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71457be..d7cb56e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # RegexSolver Python API Client [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 regular expression manipulation toolkit that lets you manipulate regular expressions as if they were sets. It provides a powerful API to perform operations like union, intersection, and difference on regex patterns, enabling advanced regex analysis and transformation. +**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. ## Installation From 585c8bd3f906f0c6ea7f09119c25100fe3b327cd Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:13:57 +0200 Subject: [PATCH 16/47] Update project --- pyproject.toml | 6 +++--- setup.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 796bf24..6367854 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,15 +8,15 @@ version = "1.1.0" authors = [ { name = "RegexSolver", email = "contact@regexsolver.com" } ] -description = "RegexSolver allows you to manipulate regular expressions as sets, enabling operations such as intersection, union, and subtraction." +description = "RegexSolver is a powerful toolkit for building, combining, and analyzing regular expressions." keywords = [ "Regular Expression", "regex", "regexp", - "set", + "pattern", "intersection", "union", - "subtraction", + "concat", "difference", "equivalence", "subset", diff --git a/setup.py b/setup.py index 10a66e1..9db4e61 100644 --- a/setup.py +++ b/setup.py @@ -3,14 +3,14 @@ setup( name="regexsolver", version="1.1.0", - description="RegexSolver allows you to manipulate regular expressions as sets, enabling operations such as intersection, union, and subtraction.", + description="RegexSolver is a powerful toolkit for building, combining, and analyzing regular expressions.", long_description=open('README.md').read(), long_description_content_type='text/markdown', author="RegexSolver", author_email="contact@regexsolver.com", url="https://github.com/RegexSolver/regexsolver-python", license="MIT", - keywords="regex regexp set intersection union subtraction difference equivalence subset nfa dfa", + keywords="regex regexp pattern intersection union difference concat equivalence subset nfa dfa", packages=find_packages(exclude=["tests", "tests.*"]), install_requires=[ From 5a5ef35322ef930be3138d1cb5c4d57e86cec82a Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:29:44 +0200 Subject: [PATCH 17/47] Add missing repeat --- regexsolver/__init__.py | 291 ++++++++++++---------- tests/assets/response_compute_repeat.json | 4 + tests/term_operation_test.py | 23 +- 3 files changed, 184 insertions(+), 134 deletions(-) create mode 100644 tests/assets/response_compute_repeat.json diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 71e95cd..22be8b3 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -173,6 +173,69 @@ def regex(cls, pattern: str) -> 'Term': """ return cls(type=TermType.REGEX, value=pattern) + # Analyze + + def equivalent(self, term: 'Term', execution_timeout=None) -> bool: + """ + Check whether this term is equivalent to another. + + Parameters: + term: The term to compare against. + execution_timeout: Timeout in milliseconds for the server. + + Returns: + True if both terms accept exactly the same language. + """ + request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._analyze_equivalent(request) + + def get_cardinality(self) -> Cardinality: + """ + Get the cardinality of this term. + + Results are cached on the instance to avoid repeated API calls. + + Returns: + A `Cardinality` object describing how many distinct strings + are matched. + """ + + if self._cardinality: + return self._cardinality + elif self._details: + return self._details.cardinality + else: + self._cardinality = RegexSolver.get_instance()._analyze_cardinality(self) + return self._cardinality + + def get_details(self) -> Details: + """ + Analyze this term and return detailed information including cardinality, + length, and whether it is empty or total. + + Results are cached on the instance to avoid repeated API calls. + """ + if self._details: + return self._details + else: + self._details = RegexSolver.get_instance()._analyze_details(self) + return self._details + + def get_dot(self) -> str: + """ + Get the GraphViz DOT representation of this term. + + Results are cached on the instance to avoid repeated API calls. + + Returns: + A DOT language string describing the automaton for this term. + """ + if self._dot: + return self._dot + else: + self._dot = RegexSolver.get_instance()._analyze_dot(self) + return self._dot + def get_fair(self) -> Optional[str]: """ Return the Fast Automaton Internal Representation (FAIR). @@ -180,6 +243,24 @@ def get_fair(self) -> Optional[str]: if self.type == TermType.FAIR: return self.value return None + + def get_length(self) -> Length: + """ + Get the length bounds of this term. + + Results are cached on the instance to avoid repeated API calls. + + Returns: + A `Length` object with the minimum and maximum string length + matched by this term. + """ + if self._length: + return self._length + elif self._length: + return self._details.length + else: + self._length = RegexSolver.get_instance()._analyze_length(self) + return self._length def get_pattern(self) -> Optional[str]: """ @@ -195,64 +276,78 @@ def get_pattern(self) -> Optional[str]: else: self._pattern = RegexSolver.get_instance()._analyze_pattern(self) return self._pattern - - def get_details(self) -> Details: + + def is_empty(self) -> bool: """ - Analyze this term and return detailed information including cardinality, - length, and whether it is empty or total. + Check whether this term matches no string. Results are cached on the instance to avoid repeated API calls. """ - if self._details: - return self._details + if self._empty: + return self._empty + elif self._details: + return self._details.empty else: - self._details = RegexSolver.get_instance()._analyze_details(self) - return self._details - - def generate_strings(self, count: int, execution_timeout=None) -> List[str]: + self._empty = RegexSolver.get_instance()._analyze_empty(self) + return self._empty + + def is_empty_string(self) -> bool: """ - Generate up to `count` example strings that match this term. - - Parameters: - count: Maximum number of unique strings to generate. - execution_timeout: Timeout in milliseconds for the server. + Check whether this term matches only the empty string. - Returns: - A list of strings matched by this term. + Results are cached on the instance to avoid repeated API calls. """ - request = GenerateStringsRequest(term=self, count=count, options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._generate_strings(request) + if self._empty_string: + return self._empty_string + else: + self._empty_string = RegexSolver.get_instance()._analyze_empty_string(self) + return self._empty_string + + def is_total(self) -> bool: + """ + Check whether this term matches all possible strings. - def intersection(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': + Results are cached on the instance to avoid repeated API calls. """ - Compute the intersection of this term with one or more other terms. + if self._total: + return self._total + elif self._details: + return self._details.total + else: + self._total = RegexSolver.get_instance()._analyze_total(self) + return self._total + + def subset(self, term: 'Term', execution_timeout=None) -> bool: + """ + Check whether this term is a subset of another. Parameters: - terms: Additional terms to intersect with. - response_format: Output format (`regex`, `fair`, or `any`). + term: The term to compare against. execution_timeout: Timeout in milliseconds for the server. Returns: - A new term representing the intersection. + True if every string matched by this term is also matched by `term`. """ - request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_intersection(request) + request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._analyze_subset(request) - def union(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': + # Compute + + def concat(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - Compute the union of this term with one or more other terms. + Concatenate this term with one or more other terms. Parameters: - terms: Terms to combine with this one. + terms: Additional terms to append in sequence. response_format: Output format (`regex`, `fair`, or `any`). execution_timeout: Timeout in milliseconds for the server. Returns: - A new term representing the union. + A new term representing the concatenation. """ request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_union(request) - + return RegexSolver.get_instance()._compute_concat(request) + def difference(self, term: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ Compute the difference between this term and another. @@ -268,141 +363,71 @@ def difference(self, term: 'Term', response_format=None, execution_timeout=None) request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) return RegexSolver.get_instance()._compute_difference(request) - def concat(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': + def intersection(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - Concatenate this term with one or more other terms. + Compute the intersection of this term with one or more other terms. Parameters: - terms: Additional terms to append in sequence. + terms: Additional terms to intersect with. response_format: Output format (`regex`, `fair`, or `any`). execution_timeout: Timeout in milliseconds for the server. Returns: - A new term representing the concatenation. + A new term representing the intersection. """ request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_concat(request) + return RegexSolver.get_instance()._compute_intersection(request) - def equivalent(self, term: 'Term', execution_timeout=None) -> bool: + def repeat(self, min: int, max: Optional[int], response_format=None, execution_timeout=None) -> 'Term': """ - Check whether this term is equivalent to another. + Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Parameters: - term: The term to compare against. + min: The lower bound of the repetition. + max: The upper bound of the repetition, if `None` the repetition is unbounded. + response_format: Output format (`regex`, `fair`, or `any`). execution_timeout: Timeout in milliseconds for the server. Returns: - True if both terms accept exactly the same language. + A new term representing the repetition. """ - request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._analyze_equivalent(request) + request = RepeatRequest(term=self, min=min, max=max, options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_repeat(request) - def subset(self, term: 'Term', execution_timeout=None) -> bool: + + def union(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': """ - Check whether this term is a subset of another. + Compute the union of this term with one or more other terms. Parameters: - term: The term to compare against. + terms: Terms to combine with this one. + response_format: Output format (`regex`, `fair`, or `any`). execution_timeout: Timeout in milliseconds for the server. Returns: - True if every string matched by this term is also matched by `term`. - """ - request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._analyze_subset(request) - - def is_empty(self) -> bool: + A new term representing the union. """ - Check whether this term matches no string. + request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._compute_union(request) - Results are cached on the instance to avoid repeated API calls. - """ - if self._empty: - return self._empty - elif self._details: - return self._details.empty - else: - self._empty = RegexSolver.get_instance()._analyze_empty(self) - return self._empty - - def is_total(self) -> bool: - """ - Check whether this term matches all possible strings. + # Generate - Results are cached on the instance to avoid repeated API calls. - """ - if self._total: - return self._total - elif self._details: - return self._details.total - else: - self._total = RegexSolver.get_instance()._analyze_total(self) - return self._total - - def is_empty_string(self) -> bool: + def generate_strings(self, count: int, execution_timeout=None) -> List[str]: """ - Check whether this term matches only the empty string. + Generate up to `count` example strings that match this term. - Results are cached on the instance to avoid repeated API calls. - """ - if self._empty_string: - return self._empty_string - else: - self._empty_string = RegexSolver.get_instance()._analyze_empty_string(self) - return self._empty_string - - def get_dot(self) -> str: - """ - Get the GraphViz DOT representation of this term. - - Results are cached on the instance to avoid repeated API calls. + Parameters: + count: Maximum number of unique strings to generate. + execution_timeout: Timeout in milliseconds for the server. Returns: - A DOT language string describing the automaton for this term. + A list of strings matched by this term. """ - if self._dot: - return self._dot - else: - self._dot = RegexSolver.get_instance()._analyze_dot(self) - return self._dot + request = GenerateStringsRequest(term=self, count=count, options=RequestOptions.from_args(execution_timeout=execution_timeout)) + return RegexSolver.get_instance()._generate_strings(request) - def get_cardinality(self) -> Cardinality: - """ - Get the cardinality of this term. - - Results are cached on the instance to avoid repeated API calls. - - Returns: - A `Cardinality` object describing how many distinct strings - are matched. - """ - - if self._cardinality: - return self._cardinality - elif self._details: - return self._details.cardinality - else: - self._cardinality = RegexSolver.get_instance()._analyze_cardinality(self) - return self._cardinality + # Other - def get_length(self) -> Length: - """ - Get the length bounds of this term. - - Results are cached on the instance to avoid repeated API calls. - - Returns: - A `Length` object with the minimum and maximum string length - matched by this term. - """ - if self._length: - return self._length - elif self._length: - return self._details.length - else: - self._length = RegexSolver.get_instance()._analyze_length(self) - return self._length - def serialize(self) -> str: """ Return a string representation of this term in the format diff --git a/tests/assets/response_compute_repeat.json b/tests/assets/response_compute_repeat.json new file mode 100644 index 0000000..6043d14 --- /dev/null +++ b/tests/assets/response_compute_repeat.json @@ -0,0 +1,4 @@ +{ + "type": "regex", + "value": "abc{3,5}" +} \ No newline at end of file diff --git a/tests/term_operation_test.py b/tests/term_operation_test.py index d814a9e..930a82a 100644 --- a/tests/term_operation_test.py +++ b/tests/term_operation_test.py @@ -8,7 +8,9 @@ class TermsOperationTest(unittest.TestCase): def setUp(self): RegexSolver.initialize("TOKEN") - + + # Analyze + def test_analyze_cardinality(self): with open('tests/assets/response_analyze_cardinality.json') as response: json_response = json.load(response) @@ -223,6 +225,8 @@ def test_analyze_subset(self): self.assertEqual(True, result) + # Compute + def test_compute_concat(self): with open('tests/assets/response_compute_concat.json') as response: json_response = json.load(response) @@ -271,6 +275,21 @@ def test_compute_intersection(self): result = term1.intersection(term2, term3, response_format=ResponseFormat.REGEX) self.assertEqual("regex=deabc", str(result)) + + def test_compute_repeat(self): + with open('tests/assets/response_compute_repeat.json') as response: + json_response = json.load(response) + with requests_mock.Mocker() as mock: + mock.post( + "https://api.regexsolver.com/api/compute/repeat", + json=json_response, status_code=200 + ) + + term = Term.regex(r"abc") + + result = term.repeat(3, 5, response_format=ResponseFormat.REGEX) + + self.assertEqual("regex=abc{3,5}", str(result)) def test_compute_union(self): with open('tests/assets/response_compute_union.json') as response: @@ -288,6 +307,8 @@ def test_compute_union(self): result = term1.union(term2, term3, response_format=ResponseFormat.REGEX) self.assertEqual("regex=(abc|de|fghi)", str(result)) + + # Generate def test_generate_strings(self): with open('tests/assets/response_generate_strings.json') as response: From 03a5bca8d548fa10a0cd477866a52759d400e785 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:52:11 +0200 Subject: [PATCH 18/47] Improve testing --- .github/workflows/python.yml | 2 + test-requirements.txt | 3 +- .../assets/response_analyze_cardinality.json | 4 - tests/assets/response_analyze_details.json | 13 - .../response_analyze_details_empty.json | 13 - .../response_analyze_details_infinite.json | 12 - tests/assets/response_analyze_dot.json | 4 - tests/assets/response_analyze_empty.json | 4 - .../assets/response_analyze_empty_string.json | 4 - tests/assets/response_analyze_equivalent.json | 4 - tests/assets/response_analyze_length.json | 5 - .../assets/response_analyze_length_empty.json | 3 - tests/assets/response_analyze_pattern.json | 4 - tests/assets/response_analyze_subset.json | 4 - tests/assets/response_analyze_total.json | 4 - tests/assets/response_compute_concat.json | 4 - tests/assets/response_compute_difference.json | 4 - .../assets/response_compute_intersection.json | 4 - tests/assets/response_compute_repeat.json | 4 - tests/assets/response_compute_union.json | 4 - tests/assets/response_generate_strings.json | 9 - tests/integration_test.py | 200 +++++++++++ tests/term_operation_test.py | 317 +----------------- 23 files changed, 205 insertions(+), 424 deletions(-) delete mode 100644 tests/assets/response_analyze_cardinality.json delete mode 100644 tests/assets/response_analyze_details.json delete mode 100644 tests/assets/response_analyze_details_empty.json delete mode 100644 tests/assets/response_analyze_details_infinite.json delete mode 100644 tests/assets/response_analyze_dot.json delete mode 100644 tests/assets/response_analyze_empty.json delete mode 100644 tests/assets/response_analyze_empty_string.json delete mode 100644 tests/assets/response_analyze_equivalent.json delete mode 100644 tests/assets/response_analyze_length.json delete mode 100644 tests/assets/response_analyze_length_empty.json delete mode 100644 tests/assets/response_analyze_pattern.json delete mode 100644 tests/assets/response_analyze_subset.json delete mode 100644 tests/assets/response_analyze_total.json delete mode 100644 tests/assets/response_compute_concat.json delete mode 100644 tests/assets/response_compute_difference.json delete mode 100644 tests/assets/response_compute_intersection.json delete mode 100644 tests/assets/response_compute_repeat.json delete mode 100644 tests/assets/response_compute_union.json delete mode 100644 tests/assets/response_generate_strings.json create mode 100644 tests/integration_test.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a9a10d5..4ab8f0b 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,4 +29,6 @@ jobs: pip install pytest - name: Run tests + env: + REGEXSOLVER_API_TOKEN: ${{ secrets.REGEXSOLVER_API_TOKEN }} run: pytest diff --git a/test-requirements.txt b/test-requirements.txt index 7a9c72b..606d9d3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ -requests_mock>=1.9.0 \ No newline at end of file +requests_mock>=1.9.0 +python-dotenv==1.1.1 \ No newline at end of file diff --git a/tests/assets/response_analyze_cardinality.json b/tests/assets/response_analyze_cardinality.json deleted file mode 100644 index 157edb5..0000000 --- a/tests/assets/response_analyze_cardinality.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "integer", - "value": 5 -} \ No newline at end of file diff --git a/tests/assets/response_analyze_details.json b/tests/assets/response_analyze_details.json deleted file mode 100644 index 07ce803..0000000 --- a/tests/assets/response_analyze_details.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "details", - "cardinality": { - "type": "integer", - "value": 2 - }, - "length": [ - 2, - 3 - ], - "empty": false, - "total": false -} \ No newline at end of file diff --git a/tests/assets/response_analyze_details_empty.json b/tests/assets/response_analyze_details_empty.json deleted file mode 100644 index f50bf22..0000000 --- a/tests/assets/response_analyze_details_empty.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "details", - "cardinality": { - "type": "integer", - "value": 0 - }, - "length": [ - null, - null - ], - "empty": true, - "total": false -} \ No newline at end of file diff --git a/tests/assets/response_analyze_details_infinite.json b/tests/assets/response_analyze_details_infinite.json deleted file mode 100644 index fe08178..0000000 --- a/tests/assets/response_analyze_details_infinite.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "details", - "cardinality": { - "type": "infinite" - }, - "length": [ - 0, - null - ], - "empty": false, - "total": true -} \ No newline at end of file diff --git a/tests/assets/response_analyze_dot.json b/tests/assets/response_analyze_dot.json deleted file mode 100644 index 5b71397..0000000 --- a/tests/assets/response_analyze_dot.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "string", - "value": "digraph G { ... }" -} \ No newline at end of file diff --git a/tests/assets/response_analyze_empty.json b/tests/assets/response_analyze_empty.json deleted file mode 100644 index 84ed493..0000000 --- a/tests/assets/response_analyze_empty.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": true -} \ No newline at end of file diff --git a/tests/assets/response_analyze_empty_string.json b/tests/assets/response_analyze_empty_string.json deleted file mode 100644 index 84ed493..0000000 --- a/tests/assets/response_analyze_empty_string.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": true -} \ No newline at end of file diff --git a/tests/assets/response_analyze_equivalent.json b/tests/assets/response_analyze_equivalent.json deleted file mode 100644 index 25147f3..0000000 --- a/tests/assets/response_analyze_equivalent.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": false -} \ No newline at end of file diff --git a/tests/assets/response_analyze_length.json b/tests/assets/response_analyze_length.json deleted file mode 100644 index 0109dd3..0000000 --- a/tests/assets/response_analyze_length.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "length", - "min": 0, - "max": 3 -} \ No newline at end of file diff --git a/tests/assets/response_analyze_length_empty.json b/tests/assets/response_analyze_length_empty.json deleted file mode 100644 index eb3a50f..0000000 --- a/tests/assets/response_analyze_length_empty.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "length" -} \ No newline at end of file diff --git a/tests/assets/response_analyze_pattern.json b/tests/assets/response_analyze_pattern.json deleted file mode 100644 index 42cb41f..0000000 --- a/tests/assets/response_analyze_pattern.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "string", - "value": "abc.*" -} \ No newline at end of file diff --git a/tests/assets/response_analyze_subset.json b/tests/assets/response_analyze_subset.json deleted file mode 100644 index 84ed493..0000000 --- a/tests/assets/response_analyze_subset.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": true -} \ No newline at end of file diff --git a/tests/assets/response_analyze_total.json b/tests/assets/response_analyze_total.json deleted file mode 100644 index 25147f3..0000000 --- a/tests/assets/response_analyze_total.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "boolean", - "value": false -} \ No newline at end of file diff --git a/tests/assets/response_compute_concat.json b/tests/assets/response_compute_concat.json deleted file mode 100644 index c316789..0000000 --- a/tests/assets/response_compute_concat.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "abcde" -} \ No newline at end of file diff --git a/tests/assets/response_compute_difference.json b/tests/assets/response_compute_difference.json deleted file mode 100644 index 478ac72..0000000 --- a/tests/assets/response_compute_difference.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "abc" -} \ No newline at end of file diff --git a/tests/assets/response_compute_intersection.json b/tests/assets/response_compute_intersection.json deleted file mode 100644 index e6b1a7a..0000000 --- a/tests/assets/response_compute_intersection.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "deabc" -} \ No newline at end of file diff --git a/tests/assets/response_compute_repeat.json b/tests/assets/response_compute_repeat.json deleted file mode 100644 index 6043d14..0000000 --- a/tests/assets/response_compute_repeat.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "abc{3,5}" -} \ No newline at end of file diff --git a/tests/assets/response_compute_union.json b/tests/assets/response_compute_union.json deleted file mode 100644 index 27dae5e..0000000 --- a/tests/assets/response_compute_union.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "regex", - "value": "(abc|de|fghi)" -} \ No newline at end of file diff --git a/tests/assets/response_generate_strings.json b/tests/assets/response_generate_strings.json deleted file mode 100644 index 9ee8883..0000000 --- a/tests/assets/response_generate_strings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "type": "strings", - "value": [ - "abcde", - "dede", - "deabc", - "abcabc" - ] -} \ No newline at end of file diff --git a/tests/integration_test.py b/tests/integration_test.py new file mode 100644 index 0000000..91013c6 --- /dev/null +++ b/tests/integration_test.py @@ -0,0 +1,200 @@ +import unittest +from dotenv import load_dotenv +from regexsolver import RegexSolver, ResponseFormat, Term + + +class IntegrationTest(unittest.TestCase): + def setUp(self): + load_dotenv() + RegexSolver.initialize() + + # Analyze + + def test_analyze_cardinality(self): + term = Term.regex(r"[0-4]") + cardinality = term.get_cardinality() + + self.assertEqual( + "Integer(5)", + str(cardinality) + ) + + def test_analyze_details(self): + term = Term.regex(r"(abc|de)") + details = term.get_details() + + self.assertEqual( + "Details[cardinality=Integer(2), length=Length[minimum=2, maximum=3], empty=False, total=False]", + str(details) + ) + + def test_analyze_details_infinite(self): + term = Term.regex(r".*") + details = term.get_details() + + self.assertEqual( + "Details[cardinality=Infinite, length=Length[minimum=0, maximum=None], empty=False, total=True]", + str(details) + ) + + def test_analyze_details_empty(self): + term = Term.regex(r"[]") + details = term.get_details() + + self.assertEqual( + "Details[cardinality=Integer(0), length=Length[minimum=None, maximum=None], empty=True, total=False]", + str(details) + ) + + def test_analyze_dot(self): + term = Term.regex(r"(abc|de)") + dot = term.get_dot() + + self.assertTrue(dot.startswith("digraph ")) + + def test_analyze_empty_string(self): + term = Term.regex(r"") + + result = term.is_empty_string() + + self.assertTrue(result) + + def test_analyze_empty(self): + term = Term.regex(r"[]") + + result = term.is_empty() + + self.assertTrue(result) + + def test_analyze_total(self): + term = Term.regex(r".*") + + result = term.is_total() + + self.assertTrue(result) + + def test_analyze_equivalent(self): + term1 = Term.regex(r"(abc|de)") + term2 = Term.fair("sLc#w-!No&(oq@Sf>X).?lI3{uh{80qWEH[#0.pHq@B-9o[LpP-a#fYI+") - - result = term1.equivalent(term2) - - self.assertEqual(False, result) - - def test_analyze_length_empty(self): - with open('tests/assets/response_analyze_length_empty.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/analyze/length", - json=json_response, status_code=200 - ) - - term = Term.regex(r"[]") - length = term.get_length() - - self.assertEqual( - "Length[minimum=None, maximum=None]", - str(length) - ) - - def test_analyze_length(self): - with open('tests/assets/response_analyze_length.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/analyze/length", - json=json_response, status_code=200 - ) - - term = Term.regex(r"(abc)?") - length = term.get_length() - - self.assertEqual( - "Length[minimum=0, maximum=3]", - str(length) - ) - - def test_analyze_pattern(self): - with open('tests/assets/response_analyze_pattern.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/analyze/pattern", - json=json_response, status_code=200 - ) - - term = Term.regex(r"abc.*") - pattern = term.get_pattern() - - self.assertEqual( - "abc.*", - str(pattern) - ) - - def test_analyze_subset(self): - with open('tests/assets/response_analyze_subset.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/analyze/subset", - json=json_response, status_code=200 - ) - - term1 = Term.regex(r"de") - term2 = Term.regex(r"(abc|de)") - - result = term1.subset(term2) - - self.assertEqual(True, result) - - # Compute - - def test_compute_concat(self): - with open('tests/assets/response_compute_concat.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/compute/concat", - json=json_response, status_code=200 - ) - - term1 = Term.regex(r"abc") - term2 = Term.regex(r"de") - - result = term1.concat(term2, response_format=ResponseFormat.REGEX) - - self.assertEqual("regex=abcde", str(result)) - - def test_compute_difference(self): - with open('tests/assets/response_compute_difference.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/compute/difference", - json=json_response, status_code=200 - ) - - term1 = Term.regex(r"(abc|de)") - term2 = Term.regex(r"de") - - result = term1.difference(term2, response_format=ResponseFormat.REGEX) - - self.assertEqual("regex=abc", str(result)) - - def test_compute_intersection(self): - with open('tests/assets/response_compute_intersection.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/compute/intersection", - json=json_response, status_code=200 - ) - - term1 = Term.regex(r"(abc|de){2}") - term2 = Term.regex(r"de.*") - term3 = Term.regex(r".*abc") - - result = term1.intersection(term2, term3, response_format=ResponseFormat.REGEX) - - self.assertEqual("regex=deabc", str(result)) - - def test_compute_repeat(self): - with open('tests/assets/response_compute_repeat.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/compute/repeat", - json=json_response, status_code=200 - ) - - term = Term.regex(r"abc") - - result = term.repeat(3, 5, response_format=ResponseFormat.REGEX) - - self.assertEqual("regex=abc{3,5}", str(result)) - - def test_compute_union(self): - with open('tests/assets/response_compute_union.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/compute/union", - json=json_response, status_code=200 - ) - - term1 = Term.regex(r"abc") - term2 = Term.regex(r"de") - term3 = Term.regex(r"fghi") - - result = term1.union(term2, term3, response_format=ResponseFormat.REGEX) - - self.assertEqual("regex=(abc|de|fghi)", str(result)) - - # Generate - - def test_generate_strings(self): - with open('tests/assets/response_generate_strings.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/api/generate/strings", - json=json_response, status_code=200 - ) - - term = Term.regex(r"(abc|de){2}") - strings = term.generate_strings(10) - - self.assertEqual(4, len(strings)) def test_error_response(self): with open('tests/assets/response_error.json') as response: From a150a829417e35da4492c8ebc21ce784bfb64c30 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:46:02 +0200 Subject: [PATCH 19/47] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7cb56e..d0c4199 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ term = Term.regex(r"abcde") result = term.union(Term.regex(r"de"), response_format=ResponseFormat.REGEX) print(result) # regex=(abc)?de -result = term.intersection(Term.regex(r"de.*"), response_format=ResponseFormat.FAIR) +result = term.union(Term.regex(r"de"), response_format=ResponseFormat.FAIR) print(result) # fair=... ``` From 85f3604ef1a9bf612969ce9982ead86a596cd9de Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:16:11 +0100 Subject: [PATCH 20/47] Remove get_details --- README.md | 1 - regexsolver/__init__.py | 172 ++++++++++++++++++++--------------- regexsolver/details.py | 74 --------------- tests/integration_test.py | 27 ------ tests/term_operation_test.py | 2 +- 5 files changed, 101 insertions(+), 175 deletions(-) delete mode 100644 regexsolver/details.py diff --git a/README.md b/README.md index d0c4199..f9ec186 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,6 @@ Timeout is best effort. The exact time is not guaranteed. | -------- | ------- | ------- | | `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | | `t.get_cardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | -| `t.get_details()` | `Details` | Returns cardinality, length bounds, and if it is empty or total. | | `t.get_dot()` | `str` | Returns a Graphviz DOT representation of the automaton for the term. | | `t.get_fair()` | `str` | Returns the FAIR of the term if defined. | | `t.get_length()` | `Length` | Returns the minimum and maximum length of matched strings. | diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 22be8b3..e43f256 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -1,13 +1,92 @@ from enum import Enum -from importlib import metadata +from typing import Any, Optional import os -from regexsolver.details import Details, Cardinality, Length - from typing import List, Optional -from pydantic import BaseModel +from pydantic import BaseModel, model_validator import requests +class Cardinality(BaseModel): + """ + Class that represent the number of possible values. + """ + type: str + value: Optional[int] = None + + def is_infinite(self) -> bool: + """ + True if it has a infinite number of values, False otherwise. + """ + return self.type == 'infinite' + + def __str__(self): + if self.type == 'infinite': + return "Infinite" + elif self.type == 'bigInteger': + return 'BigInteger' + elif self.type == 'integer': + return "Integer({})".format(self.value) + else: + return 'Unknown' + + +class Length(BaseModel): + """ + Contains the minimum and maximum length of possible values. + """ + + minimum: Optional[int] + maximum: Optional[int] + + @model_validator(mode="before") + def from_list(cls, values: Any): + if isinstance(values, dict): + return {'minimum': values.get('min'), 'maximum': values.get('max')} + + if isinstance(values, list): + if len(values) != 2: + raise ValueError("List must contain exactly two elements") + return {'minimum': values[0], 'maximum': values[1]} + + return values + + def __str__(self): + return "Length[minimum={}, maximum={}]".format( + self.minimum, + self.maximum + ) + +class ResponseFormat(str, Enum): + ANY = "any" + REGEX = "regex" + FAIR = "fair" + +class ResponseOptions(BaseModel): + format: Optional[ResponseFormat] = None + + model_config = {"use_enum_values": True} + +class ExecutionOptions(BaseModel): + timeout: Optional[int] = None + +class RequestOptions(BaseModel): + schema_version: int = 1 + response: Optional[ResponseOptions] = None + execution: Optional[ExecutionOptions] = None + + @classmethod + def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None) -> "RequestOptions | None": + response = None + if response_format: + response=ResponseOptions(format=response_format) + execution = None + if execution_timeout: + execution=ExecutionOptions(timeout=execution_timeout) + if response or execution: + return cls(response=response, execution=execution) + else: + return None + class ApiError(Exception): """ Exception raised when the API returns an error. @@ -48,7 +127,7 @@ def initialize(cls, api_token: str = None, base_url: str = None): if base_url: instance._base_url = base_url else: - instance._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com") + instance._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com/v1/") instance._headers['Authorization'] = f'Bearer {instance._api_token}' @@ -76,57 +155,54 @@ def _request(self, endpoint: str, request: BaseModel) -> dict: # Analyze - def _analyze_details(self, term: 'Term') -> Details: - return Details(**self._request('api/analyze/details', term)) - def _analyze_cardinality(self, term: 'Term') -> Cardinality: - return Cardinality(**self._request('api/analyze/cardinality', term)) + return Cardinality(**self._request('analyze/cardinality', term)) def _analyze_length(self, term: 'Term') -> Length: - return Length(**self._request('api/analyze/length', term)) + return Length(**self._request('analyze/length', term)) def _analyze_equivalent(self, request: 'MultiTermsRequest') -> bool: - return self._request('api/analyze/equivalent', request).get('value') + return self._request('analyze/equivalent', request).get('value') def _analyze_subset(self, request: 'MultiTermsRequest') -> bool: - return self._request('api/analyze/subset', request).get('value') + return self._request('analyze/subset', request).get('value') def _analyze_empty(self, term: 'Term') -> bool: - return self._request('api/analyze/empty', term).get('value') + return self._request('analyze/empty', term).get('value') def _analyze_total(self, term: 'Term') -> bool: - return self._request('api/analyze/total', term).get('value') + return self._request('analyze/total', term).get('value') def _analyze_empty_string(self, term: 'Term') -> bool: - return self._request('api/analyze/empty_string', term).get('value') + return self._request('analyze/empty_string', term).get('value') def _analyze_dot(self, term: 'Term') -> str: - return self._request('api/analyze/dot', term).get('value') + return self._request('analyze/dot', term).get('value') def _analyze_pattern(self, term: 'Term') -> str: - return self._request('api/analyze/pattern', term).get('value') + return self._request('analyze/pattern', term).get('value') # Compute def _compute_repeat(self, request: 'RepeatRequest') -> 'Term': - return Term(**self._request('api/compute/repeat', request)) + return Term(**self._request('compute/repeat', request)) def _compute_intersection(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('api/compute/intersection', request)) + return Term(**self._request('compute/intersection', request)) def _compute_union(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('api/compute/union', request)) + return Term(**self._request('compute/union', request)) def _compute_difference(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('api/compute/difference', request)) + return Term(**self._request('compute/difference', request)) def _compute_concat(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('api/compute/concat', request)) + return Term(**self._request('compute/concat', request)) # Generate def _generate_strings(self, request: 'GenerateStringsRequest') -> List[str]: - return self._request('api/generate/strings', request).get('value') + return self._request('generate/strings', request).get('value') class TermType(str, Enum): @@ -148,7 +224,6 @@ class Term(BaseModel): type: TermType value: str - _details: Optional['Details'] = None _cardinality: Optional[Cardinality] = None _length: Optional[Length] = None _empty: Optional[bool] = None @@ -202,24 +277,10 @@ def get_cardinality(self) -> Cardinality: if self._cardinality: return self._cardinality - elif self._details: - return self._details.cardinality else: self._cardinality = RegexSolver.get_instance()._analyze_cardinality(self) return self._cardinality - def get_details(self) -> Details: - """ - Analyze this term and return detailed information including cardinality, - length, and whether it is empty or total. - - Results are cached on the instance to avoid repeated API calls. - """ - if self._details: - return self._details - else: - self._details = RegexSolver.get_instance()._analyze_details(self) - return self._details def get_dot(self) -> str: """ @@ -285,8 +346,6 @@ def is_empty(self) -> bool: """ if self._empty: return self._empty - elif self._details: - return self._details.empty else: self._empty = RegexSolver.get_instance()._analyze_empty(self) return self._empty @@ -311,8 +370,6 @@ def is_total(self) -> bool: """ if self._total: return self._total - elif self._details: - return self._details.total else: self._total = RegexSolver.get_instance()._analyze_total(self) return self._total @@ -473,37 +530,8 @@ def __eq__(self, other): def __hash__(self): return hash(self.serialize()) -class ResponseFormat(str, Enum): - ANY = "any" - REGEX = "regex" - FAIR = "fair" - -class ResponseOptions(BaseModel): - format: Optional[ResponseFormat] = None - - model_config = {"use_enum_values": True} -class ExecutionOptions(BaseModel): - timeout: Optional[int] = None - -class RequestOptions(BaseModel): - schema_version: int = 1 - response: Optional[ResponseOptions] = None - execution: Optional[ExecutionOptions] = None - - @classmethod - def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None) -> "RequestOptions | None": - response = None - if response_format: - response=ResponseOptions(format=response_format) - execution = None - if execution_timeout: - execution=ExecutionOptions(timeout=execution_timeout) - if response or execution: - return cls(response=response, execution=execution) - else: - return None - + class MultiTermsRequest(BaseModel): terms: List[Term] options: Optional[RequestOptions] = None diff --git a/regexsolver/details.py b/regexsolver/details.py deleted file mode 100644 index f7741d0..0000000 --- a/regexsolver/details.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import Any, Optional - -from pydantic import BaseModel, model_validator - - -class Cardinality(BaseModel): - """ - Class that represent the number of possible values. - """ - type: str - value: Optional[int] = None - - def is_infinite(self) -> bool: - """ - True if it has a infinite number of values, False otherwise. - """ - return self.type == 'infinite' - - def __str__(self): - if self.type == 'infinite': - return "Infinite" - elif self.type == 'bigInteger': - return 'BigInteger' - elif self.type == 'integer': - return "Integer({})".format(self.value) - else: - return 'Unknown' - - -class Length(BaseModel): - """ - Contains the minimum and maximum length of possible values. - """ - - minimum: Optional[int] - maximum: Optional[int] - - @model_validator(mode="before") - def from_list(cls, values: Any): - if isinstance(values, dict): - return {'minimum': values.get('min'), 'maximum': values.get('max')} - - if isinstance(values, list): - if len(values) != 2: - raise ValueError("List must contain exactly two elements") - return {'minimum': values[0], 'maximum': values[1]} - - return values - - def __str__(self): - return "Length[minimum={}, maximum={}]".format( - self.minimum, - self.maximum - ) - - -class Details(BaseModel): - """ - Contains details about the requested Term. - """ - type: str = 'details' - - cardinality: Cardinality - length: Length - empty: bool - total: bool - - def __str__(self): - return "Details[cardinality={}, length={}, empty={}, total={}]".format( - self.cardinality, - self.length, - self.empty, - self.total - ) diff --git a/tests/integration_test.py b/tests/integration_test.py index 91013c6..c601c19 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -18,33 +18,6 @@ def test_analyze_cardinality(self): "Integer(5)", str(cardinality) ) - - def test_analyze_details(self): - term = Term.regex(r"(abc|de)") - details = term.get_details() - - self.assertEqual( - "Details[cardinality=Integer(2), length=Length[minimum=2, maximum=3], empty=False, total=False]", - str(details) - ) - - def test_analyze_details_infinite(self): - term = Term.regex(r".*") - details = term.get_details() - - self.assertEqual( - "Details[cardinality=Infinite, length=Length[minimum=0, maximum=None], empty=False, total=True]", - str(details) - ) - - def test_analyze_details_empty(self): - term = Term.regex(r"[]") - details = term.get_details() - - self.assertEqual( - "Details[cardinality=Integer(0), length=Length[minimum=None, maximum=None], empty=True, total=False]", - str(details) - ) def test_analyze_dot(self): term = Term.regex(r"(abc|de)") diff --git a/tests/term_operation_test.py b/tests/term_operation_test.py index b9826bb..bae8bb6 100644 --- a/tests/term_operation_test.py +++ b/tests/term_operation_test.py @@ -14,7 +14,7 @@ def test_error_response(self): json_response = json.load(response) with requests_mock.Mocker() as mock: mock.post( - "https://api.regexsolver.com/api/compute/intersection", + "https://api.regexsolver.com/v1/compute/intersection", json=json_response, status_code=400 ) From 5db132137569bae1ff6d08670f64ad533ca9f5f4 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:10:20 +0100 Subject: [PATCH 21/47] WIP: new client architecture --- .github/workflows/python.yml | 34 - .gitignore | 110 +- .openapi-generator-ignore | 17 + .openapi-generator/FILES | 41 + .openapi-generator/VERSION | 1 + generate-api.sh | 15 + openapitools.json | 7 + pyproject.toml | 25 +- regexsolver/__init__.py | 570 +--- regexsolver/client.py | 816 +++++ regexsolver/exceptions.py | 21 + regexsolver/generated/__init__.py | 104 + regexsolver/generated/api/__init__.py | 7 + regexsolver/generated/api/analyze_api.py | 2668 +++++++++++++++++ regexsolver/generated/api/compute_api.py | 1498 +++++++++ regexsolver/generated/api/generate_api.py | 328 ++ regexsolver/generated/api_client.py | 808 +++++ regexsolver/generated/api_response.py | 21 + regexsolver/generated/configuration.py | 581 ++++ regexsolver/generated/exceptions.py | 218 ++ regexsolver/generated/models/__init__.py | 42 + regexsolver/generated/models/boolean.py | 96 + regexsolver/generated/models/cardinality.py | 154 + .../models/cardinality200_response.py | 93 + .../models/cardinality_big_integer.py | 94 + .../generated/models/cardinality_infinite.py | 94 + .../generated/models/cardinality_integer.py | 97 + .../generated/models/concat200_response.py | 93 + .../generated/models/dot200_response.py | 93 + .../generated/models/empty200_response.py | 93 + .../generated/models/error_response.py | 89 + .../generated/models/execution_options.py | 88 + .../models/generate_strings_request.py | 99 + regexsolver/generated/models/length.py | 108 + .../generated/models/length200_response.py | 93 + .../generated/models/multi_terms_request.py | 102 + .../generated/models/repeat_request.py | 106 + .../generated/models/request_options.py | 99 + .../generated/models/response_options.py | 97 + regexsolver/generated/models/string.py | 96 + regexsolver/generated/models/strings.py | 96 + .../generated/models/strings200_response.py | 93 + regexsolver/generated/models/term.py | 140 + regexsolver/generated/models/term_fair.py | 96 + regexsolver/generated/models/term_regex.py | 96 + regexsolver/generated/models/term_request.py | 97 + .../generated/models/two_terms_request.py | 102 + regexsolver/generated/py.typed | 0 regexsolver/generated/rest.py | 226 ++ regexsolver/models/cardinality.py | 57 + regexsolver/models/length.py | 32 + regexsolver/models/response_format.py | 18 + regexsolver/models/term.py | 144 + regexsolver/models/term_properties_mixin.py | 32 + requirements.txt | 8 +- setup.py | 30 +- test-requirements.txt | 9 +- tests/assets/response_error.json | 4 - tests/integration_test.py | 173 -- tests/serialization_test.py | 79 - tests/term_operation_test.py | 34 - tests/test_client.py | 219 ++ 62 files changed, 10605 insertions(+), 996 deletions(-) delete mode 100644 .github/workflows/python.yml create mode 100644 .openapi-generator-ignore create mode 100644 .openapi-generator/FILES create mode 100644 .openapi-generator/VERSION create mode 100644 generate-api.sh create mode 100644 openapitools.json create mode 100644 regexsolver/client.py create mode 100644 regexsolver/exceptions.py create mode 100644 regexsolver/generated/__init__.py create mode 100644 regexsolver/generated/api/__init__.py create mode 100644 regexsolver/generated/api/analyze_api.py create mode 100644 regexsolver/generated/api/compute_api.py create mode 100644 regexsolver/generated/api/generate_api.py create mode 100644 regexsolver/generated/api_client.py create mode 100644 regexsolver/generated/api_response.py create mode 100644 regexsolver/generated/configuration.py create mode 100644 regexsolver/generated/exceptions.py create mode 100644 regexsolver/generated/models/__init__.py create mode 100644 regexsolver/generated/models/boolean.py create mode 100644 regexsolver/generated/models/cardinality.py create mode 100644 regexsolver/generated/models/cardinality200_response.py create mode 100644 regexsolver/generated/models/cardinality_big_integer.py create mode 100644 regexsolver/generated/models/cardinality_infinite.py create mode 100644 regexsolver/generated/models/cardinality_integer.py create mode 100644 regexsolver/generated/models/concat200_response.py create mode 100644 regexsolver/generated/models/dot200_response.py create mode 100644 regexsolver/generated/models/empty200_response.py create mode 100644 regexsolver/generated/models/error_response.py create mode 100644 regexsolver/generated/models/execution_options.py create mode 100644 regexsolver/generated/models/generate_strings_request.py create mode 100644 regexsolver/generated/models/length.py create mode 100644 regexsolver/generated/models/length200_response.py create mode 100644 regexsolver/generated/models/multi_terms_request.py create mode 100644 regexsolver/generated/models/repeat_request.py create mode 100644 regexsolver/generated/models/request_options.py create mode 100644 regexsolver/generated/models/response_options.py create mode 100644 regexsolver/generated/models/string.py create mode 100644 regexsolver/generated/models/strings.py create mode 100644 regexsolver/generated/models/strings200_response.py create mode 100644 regexsolver/generated/models/term.py create mode 100644 regexsolver/generated/models/term_fair.py create mode 100644 regexsolver/generated/models/term_regex.py create mode 100644 regexsolver/generated/models/term_request.py create mode 100644 regexsolver/generated/models/two_terms_request.py create mode 100644 regexsolver/generated/py.typed create mode 100644 regexsolver/generated/rest.py create mode 100644 regexsolver/models/cardinality.py create mode 100644 regexsolver/models/length.py create mode 100644 regexsolver/models/response_format.py create mode 100644 regexsolver/models/term.py create mode 100644 regexsolver/models/term_properties_mixin.py delete mode 100644 tests/assets/response_error.json delete mode 100644 tests/integration_test.py delete mode 100644 tests/serialization_test.py delete mode 100644 tests/term_operation_test.py create mode 100644 tests/test_client.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml deleted file mode 100644 index 4ab8f0b..0000000 --- a/.github/workflows/python.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Python checks - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r test-requirements.txt - pip install pytest - - - name: Run tests - env: - REGEXSOLVER_API_TOKEN: ${{ secrets.REGEXSOLVER_API_TOKEN }} - run: pytest diff --git a/.gitignore b/.gitignore index efa407c..65b06b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python +env/ build/ develop-eggs/ dist/ @@ -19,12 +20,9 @@ lib64/ parts/ sdist/ var/ -wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -39,17 +37,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ -.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*.cover -*.py,cover +*,cover .hypothesis/ -.pytest_cache/ -cover/ +venv/ +.venv/ +.python-version +.pytest_cache # Translations *.mo @@ -57,106 +55,12 @@ cover/ # Django stuff: *.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy # Sphinx documentation docs/_build/ # PyBuilder -.pybuilder/ target/ -# Jupyter Notebook +# Ipython Notebook .ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore new file mode 100644 index 0000000..c968472 --- /dev/null +++ b/.openapi-generator-ignore @@ -0,0 +1,17 @@ +setup.py +setup.cfg +tox.ini +git_push.sh +.travis.yml +.gitlab-ci.yml +pyproject.toml +.github/ +docs/ +test/ +README.md + + +regexsolver/__init__.py +regexsolver/client.py +regexsolver/exceptions.py +regexsolver/models/* diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES new file mode 100644 index 0000000..fe331aa --- /dev/null +++ b/.openapi-generator/FILES @@ -0,0 +1,41 @@ +.gitignore +regexsolver/generated/__init__.py +regexsolver/generated/api/__init__.py +regexsolver/generated/api/analyze_api.py +regexsolver/generated/api/compute_api.py +regexsolver/generated/api/generate_api.py +regexsolver/generated/api_client.py +regexsolver/generated/api_response.py +regexsolver/generated/configuration.py +regexsolver/generated/exceptions.py +regexsolver/generated/models/__init__.py +regexsolver/generated/models/boolean.py +regexsolver/generated/models/cardinality.py +regexsolver/generated/models/cardinality200_response.py +regexsolver/generated/models/cardinality_big_integer.py +regexsolver/generated/models/cardinality_infinite.py +regexsolver/generated/models/cardinality_integer.py +regexsolver/generated/models/concat200_response.py +regexsolver/generated/models/dot200_response.py +regexsolver/generated/models/empty200_response.py +regexsolver/generated/models/error_response.py +regexsolver/generated/models/execution_options.py +regexsolver/generated/models/generate_strings_request.py +regexsolver/generated/models/length.py +regexsolver/generated/models/length200_response.py +regexsolver/generated/models/multi_terms_request.py +regexsolver/generated/models/repeat_request.py +regexsolver/generated/models/request_options.py +regexsolver/generated/models/response_options.py +regexsolver/generated/models/string.py +regexsolver/generated/models/strings.py +regexsolver/generated/models/strings200_response.py +regexsolver/generated/models/term.py +regexsolver/generated/models/term_fair.py +regexsolver/generated/models/term_regex.py +regexsolver/generated/models/term_request.py +regexsolver/generated/models/two_terms_request.py +regexsolver/generated/py.typed +regexsolver/generated/rest.py +requirements.txt +test-requirements.txt diff --git a/.openapi-generator/VERSION b/.openapi-generator/VERSION new file mode 100644 index 0000000..2540a3a --- /dev/null +++ b/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.20.0 diff --git a/generate-api.sh b/generate-api.sh new file mode 100644 index 0000000..47d49c1 --- /dev/null +++ b/generate-api.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +SPEC_FILE="../shared/openapi.yaml" +OUT_DIR="./" +PACKAGE_NAME="regexsolver.generated" + +echo "Running openapi-generator-cli..." +openapi-generator-cli generate \ + -i "$SPEC_FILE" \ + -g python \ + -o "$OUT_DIR" \ + --additional-properties=packageName="$PACKAGE_NAME",library=asyncio + + +echo "pytest-asyncio >= 1.3.0" >> test-requirements.txt diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 0000000..c121433 --- /dev/null +++ b/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.20.0" + } +} diff --git a/pyproject.toml b/pyproject.toml index 6367854..3de9b9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,19 +25,21 @@ keywords = [ ] readme = "README.md" license = { file = "LICENSE" } +requires-python = ">=3.9" + dependencies = [ - 'requests>=2.20.0', - 'pydantic<=2.5.3, >2.4.0; python_version<"3.8"', - 'pydantic>=2.6.0; python_version>="3.8"', + "aiohttp >= 3.8.4", + "aiohttp-retry >= 2.8.3", + "python-dateutil >= 2.8.2", + "pydantic >= 2.0.0", + "typing-extensions >= 4.7.1", ] -requires-python = ">=3.7" + classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -45,6 +47,17 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] +[project.optional-dependencies] +test = [ + "pytest >= 7.2.1", + "pytest-cov >= 2.8.1", + "pytest-asyncio >= 1.3.0", + "tox >= 3.9.0", + "flake8 >= 4.0.0", + "mypy >= 1.5", + "types-python-dateutil >= 2.8.19.14", +] + [project.urls] Homepage = "https://regexsolver.com/" Issues = "https://github.com/RegexSolver/regexsolver-python/issues" diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index e43f256..a3c1d7c 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -1,548 +1,22 @@ -from enum import Enum -from typing import Any, Optional -import os - -from typing import List, Optional -from pydantic import BaseModel, model_validator -import requests - -class Cardinality(BaseModel): - """ - Class that represent the number of possible values. - """ - type: str - value: Optional[int] = None - - def is_infinite(self) -> bool: - """ - True if it has a infinite number of values, False otherwise. - """ - return self.type == 'infinite' - - def __str__(self): - if self.type == 'infinite': - return "Infinite" - elif self.type == 'bigInteger': - return 'BigInteger' - elif self.type == 'integer': - return "Integer({})".format(self.value) - else: - return 'Unknown' - - -class Length(BaseModel): - """ - Contains the minimum and maximum length of possible values. - """ - - minimum: Optional[int] - maximum: Optional[int] - - @model_validator(mode="before") - def from_list(cls, values: Any): - if isinstance(values, dict): - return {'minimum': values.get('min'), 'maximum': values.get('max')} - - if isinstance(values, list): - if len(values) != 2: - raise ValueError("List must contain exactly two elements") - return {'minimum': values[0], 'maximum': values[1]} - - return values - - def __str__(self): - return "Length[minimum={}, maximum={}]".format( - self.minimum, - self.maximum - ) - -class ResponseFormat(str, Enum): - ANY = "any" - REGEX = "regex" - FAIR = "fair" - -class ResponseOptions(BaseModel): - format: Optional[ResponseFormat] = None - - model_config = {"use_enum_values": True} - -class ExecutionOptions(BaseModel): - timeout: Optional[int] = None - -class RequestOptions(BaseModel): - schema_version: int = 1 - response: Optional[ResponseOptions] = None - execution: Optional[ExecutionOptions] = None - - @classmethod - def from_args(cls, response_format: ResponseFormat = None, execution_timeout: int = None) -> "RequestOptions | None": - response = None - if response_format: - response=ResponseOptions(format=response_format) - execution = None - if execution_timeout: - execution=ExecutionOptions(timeout=execution_timeout) - if response or execution: - return cls(response=response, execution=execution) - else: - return None - -class ApiError(Exception): - """ - Exception raised when the API returns an error. - """ - - def __init__(self, message: str): - super().__init__(f"The API returned the following error: {message}") - - -class RegexSolver: - _instance = None - - def __init__(self): - if RegexSolver._instance is not None: - raise Exception("This class is a singleton.") - else: - RegexSolver._instance = self - - self._headers = { - 'User-Agent': 'RegexSolver Python / 1.1.0', - 'Content-Type': 'application/json' - } - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = RegexSolver() - return cls._instance - - @classmethod - def initialize(cls, api_token: str = None, base_url: str = None): - instance = cls.get_instance() - if api_token: - instance._api_token = api_token - else: - instance._api_token = os.environ.get("REGEXSOLVER_API_TOKEN") or None - - if base_url: - instance._base_url = base_url - else: - instance._base_url = os.environ.get("REGEXSOLVER_BASE_URL", "https://api.regexsolver.com/v1/") - - instance._headers['Authorization'] = f'Bearer {instance._api_token}' - - def _get_request_url(self, endpoint: str) -> str: - if self._base_url.endswith('/'): - return self._base_url + endpoint - else: - return self._base_url + '/' + endpoint - - def _request(self, endpoint: str, request: BaseModel) -> dict: - response = requests.post( - self._get_request_url(endpoint), - headers=self._headers, - json=request.model_dump(exclude_none=True) - ) - - if response.ok: - return response.json() - try: - data = response.json() - msg = data.get("message", response.text) - except Exception: - msg = response.text - raise ApiError(msg) - - # Analyze - - def _analyze_cardinality(self, term: 'Term') -> Cardinality: - return Cardinality(**self._request('analyze/cardinality', term)) - - def _analyze_length(self, term: 'Term') -> Length: - return Length(**self._request('analyze/length', term)) - - def _analyze_equivalent(self, request: 'MultiTermsRequest') -> bool: - return self._request('analyze/equivalent', request).get('value') - - def _analyze_subset(self, request: 'MultiTermsRequest') -> bool: - return self._request('analyze/subset', request).get('value') - - def _analyze_empty(self, term: 'Term') -> bool: - return self._request('analyze/empty', term).get('value') - - def _analyze_total(self, term: 'Term') -> bool: - return self._request('analyze/total', term).get('value') - - def _analyze_empty_string(self, term: 'Term') -> bool: - return self._request('analyze/empty_string', term).get('value') - - def _analyze_dot(self, term: 'Term') -> str: - return self._request('analyze/dot', term).get('value') - - def _analyze_pattern(self, term: 'Term') -> str: - return self._request('analyze/pattern', term).get('value') - - # Compute - - def _compute_repeat(self, request: 'RepeatRequest') -> 'Term': - return Term(**self._request('compute/repeat', request)) - - def _compute_intersection(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('compute/intersection', request)) - - def _compute_union(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('compute/union', request)) - - def _compute_difference(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('compute/difference', request)) - - def _compute_concat(self, request: 'MultiTermsRequest') -> 'Term': - return Term(**self._request('compute/concat', request)) - - # Generate - - def _generate_strings(self, request: 'GenerateStringsRequest') -> List[str]: - return self._request('generate/strings', request).get('value') - - -class TermType(str, Enum): - FAIR = "fair" - REGEX = "regex" - - -class Term(BaseModel): - """ - Represents a term on which operations can be performed. - A term can be either: - - A regular expression (`regex`) - - A FAIR (Fast Automaton Internal Representation, `fair`) - - Convenience constructors: - - `Term.regex(pattern: str)` - - `Term.fair(fair: str)` - """ - - type: TermType - value: str - _cardinality: Optional[Cardinality] = None - _length: Optional[Length] = None - _empty: Optional[bool] = None - _total: Optional[bool] = None - _empty_string: Optional[bool] = None - _dot: Optional[str] = None - _pattern: Optional[str] = None - - model_config = {"use_enum_values": True} - - @classmethod - def fair(cls, fair: str) -> 'Term': - """ - Initialize a Fast Automaton Internal Representation (FAIR). - """ - return cls(type=TermType.FAIR, value=fair) - - @classmethod - def regex(cls, pattern: str) -> 'Term': - """ - Initialize a regex. - """ - return cls(type=TermType.REGEX, value=pattern) - - # Analyze - - def equivalent(self, term: 'Term', execution_timeout=None) -> bool: - """ - Check whether this term is equivalent to another. - - Parameters: - term: The term to compare against. - execution_timeout: Timeout in milliseconds for the server. - - Returns: - True if both terms accept exactly the same language. - """ - request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._analyze_equivalent(request) - - def get_cardinality(self) -> Cardinality: - """ - Get the cardinality of this term. - - Results are cached on the instance to avoid repeated API calls. - - Returns: - A `Cardinality` object describing how many distinct strings - are matched. - """ - - if self._cardinality: - return self._cardinality - else: - self._cardinality = RegexSolver.get_instance()._analyze_cardinality(self) - return self._cardinality - - - def get_dot(self) -> str: - """ - Get the GraphViz DOT representation of this term. - - Results are cached on the instance to avoid repeated API calls. - - Returns: - A DOT language string describing the automaton for this term. - """ - if self._dot: - return self._dot - else: - self._dot = RegexSolver.get_instance()._analyze_dot(self) - return self._dot - - def get_fair(self) -> Optional[str]: - """ - Return the Fast Automaton Internal Representation (FAIR). - """ - if self.type == TermType.FAIR: - return self.value - return None - - def get_length(self) -> Length: - """ - Get the length bounds of this term. - - Results are cached on the instance to avoid repeated API calls. - - Returns: - A `Length` object with the minimum and maximum string length - matched by this term. - """ - if self._length: - return self._length - elif self._length: - return self._details.length - else: - self._length = RegexSolver.get_instance()._analyze_length(self) - return self._length - - def get_pattern(self) -> Optional[str]: - """ - Return the regular expression pattern. - - If the term is not a regex the pattern will be resolved. - Results are cached on the instance to avoid repeated API calls. - """ - if self.type == TermType.REGEX: - return self.value - elif self._pattern: - return self._pattern - else: - self._pattern = RegexSolver.get_instance()._analyze_pattern(self) - return self._pattern - - def is_empty(self) -> bool: - """ - Check whether this term matches no string. - - Results are cached on the instance to avoid repeated API calls. - """ - if self._empty: - return self._empty - else: - self._empty = RegexSolver.get_instance()._analyze_empty(self) - return self._empty - - def is_empty_string(self) -> bool: - """ - Check whether this term matches only the empty string. - - Results are cached on the instance to avoid repeated API calls. - """ - if self._empty_string: - return self._empty_string - else: - self._empty_string = RegexSolver.get_instance()._analyze_empty_string(self) - return self._empty_string - - def is_total(self) -> bool: - """ - Check whether this term matches all possible strings. - - Results are cached on the instance to avoid repeated API calls. - """ - if self._total: - return self._total - else: - self._total = RegexSolver.get_instance()._analyze_total(self) - return self._total - - def subset(self, term: 'Term', execution_timeout=None) -> bool: - """ - Check whether this term is a subset of another. - - Parameters: - term: The term to compare against. - execution_timeout: Timeout in milliseconds for the server. - - Returns: - True if every string matched by this term is also matched by `term`. - """ - request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._analyze_subset(request) - - # Compute - - def concat(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': - """ - Concatenate this term with one or more other terms. - - Parameters: - terms: Additional terms to append in sequence. - response_format: Output format (`regex`, `fair`, or `any`). - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A new term representing the concatenation. - """ - request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_concat(request) - - def difference(self, term: 'Term', response_format=None, execution_timeout=None) -> 'Term': - """ - Compute the difference between this term and another. - - Parameters: - term: The term to subtract from this one. - response_format: Output format (`regex`, `fair`, or `any`). - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A new term representing the set difference (this - term). - """ - request = MultiTermsRequest(terms=[self, term], options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_difference(request) - - def intersection(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': - """ - Compute the intersection of this term with one or more other terms. - - Parameters: - terms: Additional terms to intersect with. - response_format: Output format (`regex`, `fair`, or `any`). - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A new term representing the intersection. - """ - request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_intersection(request) - - def repeat(self, min: int, max: Optional[int], response_format=None, execution_timeout=None) -> 'Term': - """ - Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. - - Parameters: - min: The lower bound of the repetition. - max: The upper bound of the repetition, if `None` the repetition is unbounded. - response_format: Output format (`regex`, `fair`, or `any`). - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A new term representing the repetition. - """ - request = RepeatRequest(term=self, min=min, max=max, options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_repeat(request) - - - def union(self, *terms: 'Term', response_format=None, execution_timeout=None) -> 'Term': - """ - Compute the union of this term with one or more other terms. - - Parameters: - terms: Terms to combine with this one. - response_format: Output format (`regex`, `fair`, or `any`). - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A new term representing the union. - """ - request = MultiTermsRequest(terms=[self] + list(terms), options=RequestOptions.from_args(response_format=response_format, execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._compute_union(request) - - # Generate - - def generate_strings(self, count: int, execution_timeout=None) -> List[str]: - """ - Generate up to `count` example strings that match this term. - - Parameters: - count: Maximum number of unique strings to generate. - execution_timeout: Timeout in milliseconds for the server. - - Returns: - A list of strings matched by this term. - """ - request = GenerateStringsRequest(term=self, count=count, options=RequestOptions.from_args(execution_timeout=execution_timeout)) - return RegexSolver.get_instance()._generate_strings(request) - - # Other - - def serialize(self) -> str: - """ - Return a string representation of this term in the format - `=`, which can later be parsed by `deserialize()`. - """ - if self.type == TermType.FAIR: - prefix = TermType.FAIR - elif self.type == TermType.REGEX: - prefix = TermType.REGEX - else: - raise ValueError(f"Unknown type: {self.type}") - - return prefix + "=" + self.value - - @staticmethod - def deserialize(string: str) -> Optional['Term']: - """ - Parse a string representation produced by `serialize()`. - - Parameters: - string: The serialized term, e.g. `"regex=abc"`. - - Returns: - A Term instance, or None if the input is empty or invalid. - """ - if not string or "=" not in string: - return None - prefix, value = string.split("=", 1) - if prefix == TermType.REGEX: - return Term.regex(value) - elif prefix == TermType.FAIR: - return Term.fair(value) - return None - - def __str__(self): - return self.serialize() - - def __eq__(self, other): - if isinstance(other, Term): - return self.type == other.type and self.value == other.value - return False - - def __hash__(self): - return hash(self.serialize()) - - - -class MultiTermsRequest(BaseModel): - terms: List[Term] - options: Optional[RequestOptions] = None - -class RepeatRequest(BaseModel): - term: Term - min: int - max: Optional[int] - options: Optional[RequestOptions] = None - -class GenerateStringsRequest(BaseModel): - term: Term - count: int - options: Optional[RequestOptions] = None \ No newline at end of file +from regexsolver.client import ( + AsyncRegexSolverClient, + RegexSolverClient, +) +from regexsolver.exceptions import ApiError +from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer +from regexsolver.models.length import Length +from regexsolver.models.response_format import ResponseFormat +from regexsolver.models.term import Term + +__all__ = [ + "AsyncRegexSolverClient", + "RegexSolverClient", + "Term", + "ApiError", + "BigInteger", + "Infinite", + "Integer", + "Cardinality", + "Length", + "ResponseFormat", +] diff --git a/regexsolver/client.py b/regexsolver/client.py new file mode 100644 index 0000000..5bfa293 --- /dev/null +++ b/regexsolver/client.py @@ -0,0 +1,816 @@ +import asyncio +import threading +import time +from typing import List, Optional, Union + +from regexsolver.exceptions import ApiError +from regexsolver.generated import ( + ApiException, + ErrorResponse, + ExecutionOptions, + ResponseOptions, + TwoTermsRequest, +) +from regexsolver.generated.api.analyze_api import AnalyzeApi +from regexsolver.generated.api.compute_api import ComputeApi +from regexsolver.generated.api.generate_api import GenerateApi +from regexsolver.generated.api_client import ApiClient +from regexsolver.generated.configuration import Configuration +from regexsolver.generated.models import ( + GenerateStringsRequest, + MultiTermsRequest, + RepeatRequest, + RequestOptions, + TermRequest, +) +from regexsolver.models.cardinality import BigInteger, Infinite, Integer +from regexsolver.models.length import Length +from regexsolver.models.response_format import ResponseFormat +from regexsolver.models.term import Term + + +class AsyncRegexSolverClient: + """The Asynchronous Client for RegexSolver. + + Provides non-blocking access to all RegexSolver API endpoints. + Should be instantiated using an `async with` context manager. + """ + + def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + self.configuration = Configuration(host=base_url, access_token=api_token) + self.api_client = ApiClient(self.configuration) + self.api_client.user_agent = "RegexSolver Python / 1.1.0" + + self._analyze_api = AnalyzeApi(self.api_client) + self._compute_api = ComputeApi(self.api_client) + self._generate_api = GenerateApi(self.api_client) + + self._lock = asyncio.Lock() + self._resume_time = 0.0 + + async def aclose(self): + """Closes the underlying HTTP client session.""" + await self.api_client.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.aclose() + + # --- HELPER --- + async def _execute_with_retry(self, api_method, **kwargs): + max_retries = 5 + retries = 0 + + while True: + async with self._lock: + sleep_time = self._resume_time - time.time() + + if sleep_time > 0: + await asyncio.sleep(sleep_time) + + try: + return await api_method(**kwargs) + + except ApiException as e: + if e.status == 429: + retries += 1 + if retries > max_retries: + raise ApiError( + "Max retries exceeded for 429 Too Many Requests.", + status_code=429, + ) + + async with self._lock: + sleep_time = self._resume_time - time.time() + if sleep_time <= 0: + headers = e.headers or {} + retry_after = int(headers.get("Retry-After", 1)) + self._resume_time = time.time() + retry_after + sleep_time = retry_after + + await asyncio.sleep(sleep_time) + continue + + error_msg = e.reason + + if e.body: + try: + parsed_error = ErrorResponse.from_json(e.body) + if parsed_error is not None: + error_msg = parsed_error.error + else: + error_msg = e.body + except Exception: + error_msg = e.body + + error_msg = str(error_msg) if error_msg else "Unknown API Error" + raise ApiError(error_msg, status_code=e.status, body=e.body) from None + + def _build_options( + self, + execution_timeout: Optional[int] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + ) -> RequestOptions: + options = RequestOptions(schemaVersion=1) + if execution_timeout: + options.execution = ExecutionOptions(timeout=execution_timeout) + if response_format: + options.response = ResponseOptions(format=response_format) + return options + + # --- ANALYZE --- + async def get_cardinality( + self, term: Term, execution_timeout: Optional[int] = None + ): + """Computes how many unique strings the term matches. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Cardinality: An object representing either an exact Integer, a BigInteger, or Infinite cardinality. + """ + if term._cardinality is not None: + return term._cardinality + + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.cardinality, term_request=request + ) + + generated_cardinality = response.data + actual_model = getattr( + generated_cardinality, "actual_instance", generated_cardinality + ) + + c_type = actual_model.type + if c_type == "infinite": + term._cardinality = Infinite() + elif c_type == "bigInteger": + term._cardinality = BigInteger() + elif c_type == "integer": + term._cardinality = Integer(actual_model.value) + else: + raise ValueError(f"Unknown cardinality type: {c_type}") + + term._set_properties_mixin(term._cardinality) + return term._cardinality + + async def get_length(self, term: Term, execution_timeout: Optional[int] = None): + """Computes the minimum and maximum length of strings matched by the term. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Length: An object containing `min` and `max` integers. Limits are `None` if unbounded or undefined. + """ + if term._length is not None: + return term._length + + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.length, term_request=request + ) + + generated_length = response.data + term._length = Length(min=generated_length.min, max=generated_length.max) + term._set_properties_mixin(term._length) + return term._length + + async def equivalent( + self, term1: Term, term2: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the two terms accept exactly the same language. + + Args: + term1: The first term. + term2: The second term to compare against. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if they are entirely equivalent, False otherwise. + """ + request = TwoTermsRequest( + terms=[term1._api_model, term2._api_model], + options=self._build_options(execution_timeout), + ) + response = await self._execute_with_retry( + self._analyze_api.equivalent, two_terms_request=request + ) + return response.data.value + + async def subset( + self, + term_subset: Term, + term_superset: Term, + execution_timeout: Optional[int] = None, + ) -> bool: + """Checks if the first term's language is a subset of the second term's language. + + Args: + term_subset: The term to test as the subset. + term_superset: The term representing the entire set space. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if every string matched by `term_subset` is also matched by `term_superset`. + """ + request = TwoTermsRequest( + terms=[term_subset._api_model, term_superset._api_model], + options=self._build_options(execution_timeout), + ) + response = await self._execute_with_retry( + self._analyze_api.subset, two_terms_request=request + ) + return response.data.value + + async def is_empty( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the term matches no strings at all. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the language is completely empty. + """ + if term._empty is not None: + return term._empty + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.empty, term_request=request + ) + term._empty = response.data.value + if term._empty: + term._cardinality = Integer(0) + term._length = Length(min=None, max=None) + return response.data.value + + async def is_empty_string( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the term matches only the empty string. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term strictly matches the empty string ("") and nothing else. + """ + if term._empty_string is not None: + return term._empty_string + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.empty_string, term_request=request + ) + term._empty_string = response.data.value + if term._empty_string: + term._cardinality = Integer(1) + term._length = Length(min=0, max=0) + return response.data.value + + async def is_total( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the term matches all possible strings. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term matches every possible strings. + """ + if term._total is not None: + return term._total + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.total, term_request=request + ) + term._total = response.data.value + if term._total: + term._cardinality = Infinite() + term._length = Length(min=0, max=None) + return response.data.value + + async def get_pattern( + self, term: Term, execution_timeout: Optional[int] = None + ) -> str: + """Returns a regular expression pattern that represents the term. + + Args: + term: The term to extract the pattern from. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: A valid regular expression string representing the language. + """ + if term._pattern is not None: + return term._pattern + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.pattern, term_request=request + ) + term._pattern = response.data.value + return response.data.value + + async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> str: + """Builds a Graphviz DOT representation of the term's automaton. + + Args: + term: The term to visualize. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: The raw DOT syntax for Graphviz compilation. + """ + if term._dot is not None: + return term._dot + request = TermRequest( + term=term._api_model, options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.dot, term_request=request + ) + term._dot = response.data.value + return response.data.value + + # --- COMPUTE --- + async def concat( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Concatenates the given terms sequentially. + + Args: + *terms: A dynamic list of terms to concatenate in order. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A newly computed concatenated term. + """ + request = MultiTermsRequest( + terms=[t._api_model for t in terms], + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.concat, multi_terms_request=request + ) + return Term(response.data) + + async def intersection( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the intersection of the given terms. + + Args: + *terms: A dynamic list of terms to intersect. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing only strings matched by ALL provided terms. + """ + request = MultiTermsRequest( + terms=[t._api_model for t in terms], + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.intersection, multi_terms_request=request + ) + return Term(response.data) + + async def union( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the union of the given terms. + + Args: + *terms: A dynamic list of terms to combine. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing strings matched by ANY of the provided terms. + """ + request = MultiTermsRequest( + terms=[t._api_model for t in terms], + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.union, multi_terms_request=request + ) + return Term(response.data) + + async def difference( + self, + base_term: Term, + excluded_term: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the difference between the two provided terms. + + Args: + base_term: The base language term to subtract from. + excluded_term: The term whose language should be removed from the base. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed difference term. + """ + request = TwoTermsRequest( + terms=[base_term._api_model, excluded_term._api_model], + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.difference, two_terms_request=request + ) + return Term(response.data) + + async def repeat( + self, + term: Term, + min_val: int, + max_val: Optional[int] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Repeats a term between a minimum and maximum number of times. + + Args: + term: The term to repeat. + min_val: The inclusive lower bound of repetitions. + max_val: The inclusive upper bound. If None, repetitions are unbounded. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed repeated term. + """ + request = RepeatRequest( + term=term._api_model, + min=min_val, + max=max_val, + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.repeat, repeat_request=request + ) + return Term(response.data) + + # --- GENERATE --- + async def generate_strings( + self, term: Term, count: int, execution_timeout: Optional[int] = None + ) -> List[str]: + """Generates up to `count` unique strings matched by the term. + + Args: + term: The term to sample generated strings from. + count: The maximum number of unique strings to return. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + List[str]: A list of strings that match the term. + """ + request = GenerateStringsRequest( + term=term._api_model, + count=count, + options=self._build_options(execution_timeout), + ) + response = await self._execute_with_retry( + self._generate_api.strings, generate_strings_request=request + ) + return response.data.value + + +class RegexSolverClient: + """Synchronous Client for RegexSolver. + + Exposes all endpoints synchronously by managing a background event loop. + Should be instantiated using a standard `with` context manager. + """ + + def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + self._aio = AsyncRegexSolverClient(api_token, base_url) + # Run a background event loop so sync methods don't crash in Jupyter/FastAPI + self._loop = asyncio.new_event_loop() + self._thread = threading.Thread(target=self._loop.run_forever, daemon=True) + self._thread.start() + + def _run_sync(self, coro): + """Helper to execute async methods safely from the sync wrapper.""" + future = asyncio.run_coroutine_threadsafe(coro, self._loop) + return future.result() + + def close(self): + """Closes the underlying HTTP client session and stops the background thread.""" + self._run_sync(self._aio.aclose()) + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # --- ANALYZE --- + def get_cardinality(self, term: Term, execution_timeout: Optional[int] = None): + """Computes how many unique strings the term matches. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Cardinality: An object representing either an exact Integer, a BigInteger, or Infinite cardinality. + """ + return self._run_sync(self._aio.get_cardinality(term, execution_timeout)) + + def get_length(self, term: Term, execution_timeout: Optional[int] = None): + """Computes the minimum and maximum length of strings matched by the term. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Length: An object containing `min` and `max` integers. Limits are `None` if unbounded or undefined. + """ + return self._run_sync(self._aio.get_length(term, execution_timeout)) + + def equivalent( + self, term1: Term, term2: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the two terms accept exactly the same language. + + Args: + term1: The first term. + term2: The second term to compare against. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if they are entirely equivalent, False otherwise. + """ + return self._run_sync(self._aio.equivalent(term1, term2, execution_timeout)) + + def subset( + self, + term_subset: Term, + term_superset: Term, + execution_timeout: Optional[int] = None, + ) -> bool: + """Checks if the first term's language is a subset of the second term's language. + + Args: + term_subset: The term to test as the subset. + term_superset: The term representing the entire set space. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if every string matched by `term_subset` is also matched by `term_superset`. + """ + return self._run_sync( + self._aio.subset(term_subset, term_superset, execution_timeout) + ) + + def is_empty(self, term: Term, execution_timeout: Optional[int] = None) -> bool: + """Checks if the term matches no strings at all. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the language is completely empty. + """ + return self._run_sync(self._aio.is_empty(term, execution_timeout)) + + def is_empty_string( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the term matches only the empty string. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term strictly matches the empty string ("") and nothing else. + """ + return self._run_sync(self._aio.is_empty_string(term, execution_timeout)) + + def is_total(self, term: Term, execution_timeout: Optional[int] = None) -> bool: + """Checks if the term matches all possible strings. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term matches every possible strings. + """ + return self._run_sync(self._aio.is_total(term, execution_timeout)) + + def get_pattern(self, term: Term, execution_timeout: Optional[int] = None) -> str: + """Returns a regular expression pattern that represents the term. + + Args: + term: The term to extract the pattern from. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: A valid regular expression string representing the language. + """ + return self._run_sync(self._aio.get_pattern(term, execution_timeout)) + + def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> str: + """Builds a Graphviz DOT representation of the term's automaton. + + Args: + term: The term to visualize. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: The raw DOT syntax for Graphviz compilation. + """ + return self._run_sync(self._aio.get_dot(term, execution_timeout)) + + # --- COMPUTE --- + def concat( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Concatenates the given terms sequentially. + + Args: + *terms: A dynamic list of terms to concatenate in order. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A newly computed concatenated term. + """ + return self._run_sync( + self._aio.concat( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def intersection( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the intersection of the given terms. + + Args: + *terms: A dynamic list of terms to intersect. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing only strings matched by ALL provided terms. + """ + return self._run_sync( + self._aio.intersection( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def union( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the union of the given terms. + + Args: + *terms: A dynamic list of terms to combine. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing strings matched by ANY of the provided terms. + """ + return self._run_sync( + self._aio.union( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def difference( + self, + base_term: Term, + excluded_term: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the difference between the two provided terms. + + Args: + base_term: The base language term to subtract from. + excluded_term: The term whose language should be removed from the base. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed difference term. + """ + return self._run_sync( + self._aio.difference( + base_term, + excluded_term, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def repeat( + self, + term: Term, + min_val: int, + max_val: Optional[int] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Repeats a term between a minimum and maximum number of times. + + Args: + term: The term to repeat. + min_val: The inclusive lower bound of repetitions. + max_val: The inclusive upper bound. If None, repetitions are unbounded. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed repeated term. + """ + return self._run_sync( + self._aio.repeat( + term, + min_val, + max_val, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + # --- GENERATE --- + def generate_strings( + self, term: Term, count: int, execution_timeout: Optional[int] = None + ) -> List[str]: + """Generates up to `count` unique strings matched by the term. + + Args: + term: The term to sample generated strings from. + count: The maximum number of unique strings to return. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + List[str]: A list of strings that match the term. + """ + return self._run_sync( + self._aio.generate_strings(term, count, execution_timeout) + ) diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py new file mode 100644 index 0000000..30ad429 --- /dev/null +++ b/regexsolver/exceptions.py @@ -0,0 +1,21 @@ +from typing import Optional + + +class RegexSolverError(Exception): + """Base exception for all RegexSolver errors.""" + + pass + + +class ApiError(RegexSolverError): + """Raised when the RegexSolver API returns an error response.""" + + def __init__( + self, + message: str, + status_code: Optional[int] = None, + body: Optional[str] = None, + ): + super().__init__(message) + self.status_code = status_code + self.body = body diff --git a/regexsolver/generated/__init__.py b/regexsolver/generated/__init__.py new file mode 100644 index 0000000..4180f5d --- /dev/null +++ b/regexsolver/generated/__init__.py @@ -0,0 +1,104 @@ +# coding: utf-8 + +# flake8: noqa + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +__version__ = "1.0.0" + +# Define package exports +__all__ = [ + "AnalyzeApi", + "ComputeApi", + "GenerateApi", + "ApiResponse", + "ApiClient", + "Configuration", + "OpenApiException", + "ApiTypeError", + "ApiValueError", + "ApiKeyError", + "ApiAttributeError", + "ApiException", + "Boolean", + "Cardinality", + "Cardinality200Response", + "CardinalityBigInteger", + "CardinalityInfinite", + "CardinalityInteger", + "Concat200Response", + "Dot200Response", + "Empty200Response", + "ErrorResponse", + "ExecutionOptions", + "GenerateStringsRequest", + "Length", + "Length200Response", + "MultiTermsRequest", + "RepeatRequest", + "RequestOptions", + "ResponseOptions", + "String", + "Strings", + "Strings200Response", + "Term", + "TermFair", + "TermRegex", + "TermRequest", + "TwoTermsRequest", +] + +# import apis into sdk package +from regexsolver.generated.api.analyze_api import AnalyzeApi as AnalyzeApi +from regexsolver.generated.api.compute_api import ComputeApi as ComputeApi +from regexsolver.generated.api.generate_api import GenerateApi as GenerateApi + +# import ApiClient +from regexsolver.generated.api_response import ApiResponse as ApiResponse +from regexsolver.generated.api_client import ApiClient as ApiClient +from regexsolver.generated.configuration import Configuration as Configuration +from regexsolver.generated.exceptions import OpenApiException as OpenApiException +from regexsolver.generated.exceptions import ApiTypeError as ApiTypeError +from regexsolver.generated.exceptions import ApiValueError as ApiValueError +from regexsolver.generated.exceptions import ApiKeyError as ApiKeyError +from regexsolver.generated.exceptions import ApiAttributeError as ApiAttributeError +from regexsolver.generated.exceptions import ApiException as ApiException + +# import models into sdk package +from regexsolver.generated.models.boolean import Boolean as Boolean +from regexsolver.generated.models.cardinality import Cardinality as Cardinality +from regexsolver.generated.models.cardinality200_response import Cardinality200Response as Cardinality200Response +from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger as CardinalityBigInteger +from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite as CardinalityInfinite +from regexsolver.generated.models.cardinality_integer import CardinalityInteger as CardinalityInteger +from regexsolver.generated.models.concat200_response import Concat200Response as Concat200Response +from regexsolver.generated.models.dot200_response import Dot200Response as Dot200Response +from regexsolver.generated.models.empty200_response import Empty200Response as Empty200Response +from regexsolver.generated.models.error_response import ErrorResponse as ErrorResponse +from regexsolver.generated.models.execution_options import ExecutionOptions as ExecutionOptions +from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest +from regexsolver.generated.models.length import Length as Length +from regexsolver.generated.models.length200_response import Length200Response as Length200Response +from regexsolver.generated.models.multi_terms_request import MultiTermsRequest as MultiTermsRequest +from regexsolver.generated.models.repeat_request import RepeatRequest as RepeatRequest +from regexsolver.generated.models.request_options import RequestOptions as RequestOptions +from regexsolver.generated.models.response_options import ResponseOptions as ResponseOptions +from regexsolver.generated.models.string import String as String +from regexsolver.generated.models.strings import Strings as Strings +from regexsolver.generated.models.strings200_response import Strings200Response as Strings200Response +from regexsolver.generated.models.term import Term as Term +from regexsolver.generated.models.term_fair import TermFair as TermFair +from regexsolver.generated.models.term_regex import TermRegex as TermRegex +from regexsolver.generated.models.term_request import TermRequest as TermRequest +from regexsolver.generated.models.two_terms_request import TwoTermsRequest as TwoTermsRequest + diff --git a/regexsolver/generated/api/__init__.py b/regexsolver/generated/api/__init__.py new file mode 100644 index 0000000..e5e55a6 --- /dev/null +++ b/regexsolver/generated/api/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa + +# import apis into api package +from regexsolver.generated.api.analyze_api import AnalyzeApi +from regexsolver.generated.api.compute_api import ComputeApi +from regexsolver.generated.api.generate_api import GenerateApi + diff --git a/regexsolver/generated/api/analyze_api.py b/regexsolver/generated/api/analyze_api.py new file mode 100644 index 0000000..9bcbf2c --- /dev/null +++ b/regexsolver/generated/api/analyze_api.py @@ -0,0 +1,2668 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from regexsolver.generated.models.cardinality200_response import Cardinality200Response +from regexsolver.generated.models.dot200_response import Dot200Response +from regexsolver.generated.models.empty200_response import Empty200Response +from regexsolver.generated.models.length200_response import Length200Response +from regexsolver.generated.models.term_request import TermRequest +from regexsolver.generated.models.two_terms_request import TwoTermsRequest + +from regexsolver.generated.api_client import ApiClient, RequestSerialized +from regexsolver.generated.api_response import ApiResponse +from regexsolver.generated.rest import RESTResponseType + + +class AnalyzeApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + async def cardinality( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Cardinality200Response: + """Cardinality + + Compute how many strings the term matches. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._cardinality_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Cardinality200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def cardinality_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Cardinality200Response]: + """Cardinality + + Compute how many strings the term matches. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._cardinality_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Cardinality200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def cardinality_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Cardinality + + Compute how many strings the term matches. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._cardinality_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Cardinality200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _cardinality_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/cardinality', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def dot( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Dot200Response: + """GraphViz Dot + + Build a Graphviz DOT representation of the term's automaton. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._dot_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def dot_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Dot200Response]: + """GraphViz Dot + + Build a Graphviz DOT representation of the term's automaton. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._dot_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def dot_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """GraphViz Dot + + Build a Graphviz DOT representation of the term's automaton. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._dot_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _dot_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/dot', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def empty( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Empty + + Check if the term matches no strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def empty_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Empty + + Check if the term matches no strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def empty_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Empty + + Check if the term matches no strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _empty_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/empty', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def empty_string( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Empty String Only + + Check if the term matches only the empty string. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_string_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def empty_string_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Empty String Only + + Check if the term matches only the empty string. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_string_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def empty_string_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Empty String Only + + Check if the term matches only the empty string. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._empty_string_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _empty_string_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/empty_string', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def equivalent( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Equivalent + + Check if the two terms accept exactly the same language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._equivalent_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def equivalent_with_http_info( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Equivalent + + Check if the two terms accept exactly the same language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._equivalent_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def equivalent_without_preload_content( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Equivalent + + Check if the two terms accept exactly the same language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._equivalent_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _equivalent_serialize( + self, + two_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if two_terms_request is not None: + _body_params = two_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/equivalent', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def length( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Length200Response: + """Length + + Compute the minimum and maximum length of strings matched by the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._length_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Length200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def length_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Length200Response]: + """Length + + Compute the minimum and maximum length of strings matched by the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._length_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Length200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def length_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Length + + Compute the minimum and maximum length of strings matched by the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._length_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Length200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _length_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/length', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def pattern( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Dot200Response: + """Pattern + + Return a regular expression pattern that represents the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._pattern_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def pattern_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Dot200Response]: + """Pattern + + Return a regular expression pattern that represents the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._pattern_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def pattern_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Pattern + + Return a regular expression pattern that represents the term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._pattern_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Dot200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _pattern_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/pattern', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def subset( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Subset + + Check if the first term's language is a subset of the second term's language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._subset_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def subset_with_http_info( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Subset + + Check if the first term's language is a subset of the second term's language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._subset_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def subset_without_preload_content( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Subset + + Check if the first term's language is a subset of the second term's language. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._subset_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _subset_serialize( + self, + two_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if two_terms_request is not None: + _body_params = two_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/subset', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def total( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Totality + + Check if the term matches all the possible strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._total_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def total_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Totality + + Check if the term matches all the possible strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._total_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def total_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Totality + + Check if the term matches all the possible strings. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._total_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _total_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/total', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regexsolver/generated/api/compute_api.py b/regexsolver/generated/api/compute_api.py new file mode 100644 index 0000000..f6f7d77 --- /dev/null +++ b/regexsolver/generated/api/compute_api.py @@ -0,0 +1,1498 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from regexsolver.generated.models.concat200_response import Concat200Response +from regexsolver.generated.models.multi_terms_request import MultiTermsRequest +from regexsolver.generated.models.repeat_request import RepeatRequest +from regexsolver.generated.models.two_terms_request import TwoTermsRequest + +from regexsolver.generated.api_client import ApiClient, RequestSerialized +from regexsolver.generated.api_response import ApiResponse +from regexsolver.generated.rest import RESTResponseType + + +class ComputeApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + async def concat( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Concatenation + + Concatenate the given terms in order. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._concat_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def concat_with_http_info( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Concatenation + + Concatenate the given terms in order. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._concat_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def concat_without_preload_content( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Concatenation + + Concatenate the given terms in order. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._concat_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _concat_serialize( + self, + multi_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if multi_terms_request is not None: + _body_params = multi_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/concat', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def difference( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Difference + + Computes the difference between the two provided terms. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._difference_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def difference_with_http_info( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Difference + + Computes the difference between the two provided terms. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._difference_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def difference_without_preload_content( + self, + two_terms_request: TwoTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Difference + + Computes the difference between the two provided terms. + + :param two_terms_request: (required) + :type two_terms_request: TwoTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._difference_serialize( + two_terms_request=two_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _difference_serialize( + self, + two_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if two_terms_request is not None: + _body_params = two_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/difference', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def intersection( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Intersection + + Computes the intersection of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._intersection_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def intersection_with_http_info( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Intersection + + Computes the intersection of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._intersection_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def intersection_without_preload_content( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Intersection + + Computes the intersection of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._intersection_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _intersection_serialize( + self, + multi_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if multi_terms_request is not None: + _body_params = multi_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/intersection', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def repeat( + self, + repeat_request: RepeatRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Repeat + + Repeat a term between 'min' and 'max' times. + + :param repeat_request: (required) + :type repeat_request: RepeatRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._repeat_serialize( + repeat_request=repeat_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def repeat_with_http_info( + self, + repeat_request: RepeatRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Repeat + + Repeat a term between 'min' and 'max' times. + + :param repeat_request: (required) + :type repeat_request: RepeatRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._repeat_serialize( + repeat_request=repeat_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def repeat_without_preload_content( + self, + repeat_request: RepeatRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Repeat + + Repeat a term between 'min' and 'max' times. + + :param repeat_request: (required) + :type repeat_request: RepeatRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._repeat_serialize( + repeat_request=repeat_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _repeat_serialize( + self, + repeat_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if repeat_request is not None: + _body_params = repeat_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/repeat', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + + @validate_call + async def union( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Union + + Computes the union of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._union_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def union_with_http_info( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Union + + Computes the union of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._union_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def union_without_preload_content( + self, + multi_terms_request: MultiTermsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Union + + Computes the union of the given terms. + + :param multi_terms_request: (required) + :type multi_terms_request: MultiTermsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._union_serialize( + multi_terms_request=multi_terms_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _union_serialize( + self, + multi_terms_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if multi_terms_request is not None: + _body_params = multi_terms_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/union', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regexsolver/generated/api/generate_api.py b/regexsolver/generated/api/generate_api.py new file mode 100644 index 0000000..f7500a8 --- /dev/null +++ b/regexsolver/generated/api/generate_api.py @@ -0,0 +1,328 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest +from regexsolver.generated.models.strings200_response import Strings200Response + +from regexsolver.generated.api_client import ApiClient, RequestSerialized +from regexsolver.generated.api_response import ApiResponse +from regexsolver.generated.rest import RESTResponseType + + +class GenerateApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + async def strings( + self, + generate_strings_request: GenerateStringsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Strings200Response: + """Strings + + Generate up to 'count' unique strings matched by the term. + + :param generate_strings_request: (required) + :type generate_strings_request: GenerateStringsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._strings_serialize( + generate_strings_request=generate_strings_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Strings200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def strings_with_http_info( + self, + generate_strings_request: GenerateStringsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Strings200Response]: + """Strings + + Generate up to 'count' unique strings matched by the term. + + :param generate_strings_request: (required) + :type generate_strings_request: GenerateStringsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._strings_serialize( + generate_strings_request=generate_strings_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Strings200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def strings_without_preload_content( + self, + generate_strings_request: GenerateStringsRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Strings + + Generate up to 'count' unique strings matched by the term. + + :param generate_strings_request: (required) + :type generate_strings_request: GenerateStringsRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._strings_serialize( + generate_strings_request=generate_strings_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Strings200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _strings_serialize( + self, + generate_strings_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if generate_strings_request is not None: + _body_params = generate_strings_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/generate/strings', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regexsolver/generated/api_client.py b/regexsolver/generated/api_client.py new file mode 100644 index 0000000..19d1ede --- /dev/null +++ b/regexsolver/generated/api_client.py @@ -0,0 +1,808 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + + +import datetime +from dateutil.parser import parse +from enum import Enum +import decimal +import json +import mimetypes +import os +import re +import tempfile +import uuid + +from urllib.parse import quote +from typing import Tuple, Optional, List, Dict, Union +from pydantic import SecretStr + +from regexsolver.generated.configuration import Configuration +from regexsolver.generated.api_response import ApiResponse, T as ApiResponseT +import regexsolver.generated.models +from regexsolver.generated import rest +from regexsolver.generated.exceptions import ( + ApiValueError, + ApiException, + BadRequestException, + UnauthorizedException, + ForbiddenException, + NotFoundException, + ServiceException +) + +RequestSerialized = Tuple[str, str, Dict[str, str], Optional[str], List[str]] + +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + """ + + PRIMITIVE_TYPES = (float, bool, bytes, str, int) + NATIVE_TYPES_MAPPING = { + 'int': int, + 'long': int, # TODO remove as only py3 is supported? + 'float': float, + 'str': str, + 'bool': bool, + 'date': datetime.date, + 'datetime': datetime.datetime, + 'decimal': decimal.Decimal, + 'object': object, + } + _pool = None + + def __init__( + self, + configuration=None, + header_name=None, + header_value=None, + cookie=None + ) -> None: + # use default configuration if none is provided + if configuration is None: + configuration = Configuration.get_default() + self.configuration = configuration + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + self.client_side_validation = configuration.client_side_validation + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.close() + + async def close(self): + await self.rest_client.close() + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + + _default = None + + @classmethod + def get_default(cls): + """Return new instance of ApiClient. + + This method returns newly created, based on default constructor, + object of ApiClient class or returns a copy of default + ApiClient. + + :return: The ApiClient object. + """ + if cls._default is None: + cls._default = ApiClient() + return cls._default + + @classmethod + def set_default(cls, default): + """Set default instance of ApiClient. + + It stores default ApiClient. + + :param default: object of ApiClient. + """ + cls._default = default + + def param_serialize( + self, + method, + resource_path, + path_params=None, + query_params=None, + header_params=None, + body=None, + post_params=None, + files=None, auth_settings=None, + collection_formats=None, + _host=None, + _request_auth=None + ) -> RequestSerialized: + + """Builds the HTTP request params needed by the request. + :param method: Method to call. + :param resource_path: Path to method endpoint. + :param path_params: Path parameters in the url. + :param query_params: Query parameters in the url. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings list: Auth Settings names for the request. + :param files dict: key -> filename, value -> filepath, + for `multipart/form-data`. + :param collection_formats: dict of collection formats for path, query, + header, and post parameters. + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the authentication + in the spec for a single request. + :return: tuple of form (path, http_method, query_params, header_params, + body, post_params, files) + """ + + config = self.configuration + + # header parameters + header_params = header_params or {} + header_params.update(self.default_headers) + if self.cookie: + header_params['Cookie'] = self.cookie + if header_params: + header_params = self.sanitize_for_serialization(header_params) + header_params = dict( + self.parameters_to_tuples(header_params,collection_formats) + ) + + # path parameters + if path_params: + path_params = self.sanitize_for_serialization(path_params) + path_params = self.parameters_to_tuples( + path_params, + collection_formats + ) + for k, v in path_params: + # specified safe chars, encode everything + resource_path = resource_path.replace( + '{%s}' % k, + quote(str(v), safe=config.safe_chars_for_path_param) + ) + + # post parameters + if post_params or files: + post_params = post_params if post_params else [] + post_params = self.sanitize_for_serialization(post_params) + post_params = self.parameters_to_tuples( + post_params, + collection_formats + ) + if files: + post_params.extend(self.files_parameters(files)) + + # auth setting + self.update_params_for_auth( + header_params, + query_params, + auth_settings, + resource_path, + method, + body, + request_auth=_request_auth + ) + + # body + if body: + body = self.sanitize_for_serialization(body) + + # request url + if _host is None or self.configuration.ignore_operation_servers: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = _host + resource_path + + # query parameters + if query_params: + query_params = self.sanitize_for_serialization(query_params) + url_query = self.parameters_to_url_query( + query_params, + collection_formats + ) + url += "?" + url_query + + return method, url, header_params, body, post_params + + + async def call_api( + self, + method, + url, + header_params=None, + body=None, + post_params=None, + _request_timeout=None + ) -> rest.RESTResponse: + """Makes the HTTP request (synchronous) + :param method: Method to call. + :param url: Path to method endpoint. + :param header_params: Header parameters to be + placed in the request header. + :param body: Request body. + :param post_params dict: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param _request_timeout: timeout setting for this request. + :return: RESTResponse + """ + + try: + # perform request and return response + response_data = await self.rest_client.request( + method, url, + headers=header_params, + body=body, post_params=post_params, + _request_timeout=_request_timeout + ) + + except ApiException as e: + raise e + + return response_data + + def response_deserialize( + self, + response_data: rest.RESTResponse, + response_types_map: Optional[Dict[str, ApiResponseT]]=None + ) -> ApiResponse[ApiResponseT]: + """Deserializes response into an object. + :param response_data: RESTResponse object to be deserialized. + :param response_types_map: dict of response types. + :return: ApiResponse + """ + + msg = "RESTResponse.read() must be called before passing it to response_deserialize()" + assert response_data.data is not None, msg + + response_type = response_types_map.get(str(response_data.status), None) + if not response_type and isinstance(response_data.status, int) and 100 <= response_data.status <= 599: + # if not found, look for '1XX', '2XX', etc. + response_type = response_types_map.get(str(response_data.status)[0] + "XX", None) + + # deserialize response data + response_text = None + return_data = None + try: + if response_type == "bytearray": + return_data = response_data.data + elif response_type == "file": + return_data = self.__deserialize_file(response_data) + elif response_type is not None: + match = None + content_type = response_data.headers.get('content-type') + if content_type is not None: + match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) + encoding = match.group(1) if match else "utf-8" + response_text = response_data.data.decode(encoding) + return_data = self.deserialize(response_text, response_type, content_type) + finally: + if not 200 <= response_data.status <= 299: + raise ApiException.from_response( + http_resp=response_data, + body=response_text, + data=return_data, + ) + + return ApiResponse( + status_code = response_data.status, + data = return_data, + headers = response_data.headers, + raw_data = response_data.data + ) + + def sanitize_for_serialization(self, obj): + """Builds a JSON POST object. + + If obj is None, return None. + If obj is SecretStr, return obj.get_secret_value() + If obj is str, int, long, float, bool, return directly. + If obj is datetime.datetime, datetime.date + convert to string in iso8601 format. + If obj is decimal.Decimal return string representation. + If obj is list, sanitize each element in the list. + If obj is dict, return the dict. + If obj is OpenAPI model, return the properties dict. + + :param obj: The data to serialize. + :return: The serialized form of data. + """ + if obj is None: + return None + elif isinstance(obj, Enum): + return obj.value + elif isinstance(obj, SecretStr): + return obj.get_secret_value() + elif isinstance(obj, self.PRIMITIVE_TYPES): + return obj + elif isinstance(obj, uuid.UUID): + return str(obj) + elif isinstance(obj, list): + return [ + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ] + elif isinstance(obj, tuple): + return tuple( + self.sanitize_for_serialization(sub_obj) for sub_obj in obj + ) + elif isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) + + elif isinstance(obj, dict): + obj_dict = obj + else: + # Convert model obj to dict except + # attributes `openapi_types`, `attribute_map` + # and attributes which value is not None. + # Convert attribute name to json key in + # model definition for request. + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + obj_dict = obj.to_dict() + else: + obj_dict = obj.__dict__ + + if isinstance(obj_dict, list): + # here we handle instances that can either be a list or something else, and only became a real list by calling to_dict() + return self.sanitize_for_serialization(obj_dict) + + return { + key: self.sanitize_for_serialization(val) + for key, val in obj_dict.items() + } + + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): + """Deserializes response into an object. + + :param response: RESTResponse object to be deserialized. + :param response_type: class literal for + deserialized object, or string of class name. + :param content_type: content type of response. + + :return: deserialized object. + """ + + # fetch data from response object + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif re.match(r'^text\/[a-z.+-]+\s*(;|$)', content_type, re.IGNORECASE): + data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) + + return self.__deserialize(data, response_type) + + def __deserialize(self, data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if isinstance(klass, str): + if klass.startswith('List['): + m = re.match(r'List\[(.*)]', klass) + assert m is not None, "Malformed List type definition" + sub_kls = m.group(1) + return [self.__deserialize(sub_data, sub_kls) + for sub_data in data] + + if klass.startswith('Dict['): + m = re.match(r'Dict\[([^,]*), (.*)]', klass) + assert m is not None, "Malformed Dict type definition" + sub_kls = m.group(2) + return {k: self.__deserialize(v, sub_kls) + for k, v in data.items()} + + # convert str to class + if klass in self.NATIVE_TYPES_MAPPING: + klass = self.NATIVE_TYPES_MAPPING[klass] + else: + klass = getattr(regexsolver.generated.models, klass) + + if klass in self.PRIMITIVE_TYPES: + return self.__deserialize_primitive(data, klass) + elif klass is object: + return self.__deserialize_object(data) + elif klass is datetime.date: + return self.__deserialize_date(data) + elif klass is datetime.datetime: + return self.__deserialize_datetime(data) + elif klass is decimal.Decimal: + return decimal.Decimal(data) + elif issubclass(klass, Enum): + return self.__deserialize_enum(data, klass) + else: + return self.__deserialize_model(data, klass) + + def parameters_to_tuples(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: Parameters as list of tuples, collections formatted + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, value) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(str(value) for value in v))) + else: + new_params.append((k, v)) + return new_params + + def parameters_to_url_query(self, params, collection_formats): + """Get parameters as list of tuples, formatting collections. + + :param params: Parameters as dict or list of two-tuples + :param dict collection_formats: Parameter collection formats + :return: URL query string (e.g. a=Hello%20World&b=123) + """ + new_params: List[Tuple[str, str]] = [] + if collection_formats is None: + collection_formats = {} + for k, v in params.items() if isinstance(params, dict) else params: + if isinstance(v, bool): + v = str(v).lower() + if isinstance(v, (int, float)): + v = str(v) + if isinstance(v, dict): + v = json.dumps(v) + + if k in collection_formats: + collection_format = collection_formats[k] + if collection_format == 'multi': + new_params.extend((k, quote(str(value))) for value in v) + else: + if collection_format == 'ssv': + delimiter = ' ' + elif collection_format == 'tsv': + delimiter = '\t' + elif collection_format == 'pipes': + delimiter = '|' + else: # csv is the default + delimiter = ',' + new_params.append( + (k, delimiter.join(quote(str(value)) for value in v)) + ) + else: + new_params.append((k, quote(str(v)))) + + return "&".join(["=".join(map(str, item)) for item in new_params]) + + def files_parameters( + self, + files: Dict[str, Union[str, bytes, List[str], List[bytes], Tuple[str, bytes]]], + ): + """Builds form parameters. + + :param files: File parameters. + :return: Form parameters with files. + """ + params = [] + for k, v in files.items(): + if isinstance(v, str): + with open(v, 'rb') as f: + filename = os.path.basename(f.name) + filedata = f.read() + elif isinstance(v, bytes): + filename = k + filedata = v + elif isinstance(v, tuple): + filename, filedata = v + elif isinstance(v, list): + for file_param in v: + params.extend(self.files_parameters({k: file_param})) + continue + else: + raise ValueError("Unsupported file value") + mimetype = ( + mimetypes.guess_type(filename)[0] + or 'application/octet-stream' + ) + params.append( + tuple([k, tuple([filename, filedata, mimetype])]) + ) + return params + + def select_header_accept(self, accepts: List[str]) -> Optional[str]: + """Returns `Accept` based on an array of accepts provided. + + :param accepts: List of headers. + :return: Accept (e.g. application/json). + """ + if not accepts: + return None + + for accept in accepts: + if re.search('json', accept, re.IGNORECASE): + return accept + + return accepts[0] + + def select_header_content_type(self, content_types): + """Returns `Content-Type` based on an array of content_types provided. + + :param content_types: List of content-types. + :return: Content-Type (e.g. application/json). + """ + if not content_types: + return None + + for content_type in content_types: + if re.search('json', content_type, re.IGNORECASE): + return content_type + + return content_types[0] + + def update_params_for_auth( + self, + headers, + queries, + auth_settings, + resource_path, + method, + body, + request_auth=None + ) -> None: + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :param auth_settings: Authentication setting identifiers list. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param request_auth: if set, the provided settings will + override the token in the configuration. + """ + if not auth_settings: + return + + if request_auth: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + request_auth + ) + else: + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + self._apply_auth_params( + headers, + queries, + resource_path, + method, + body, + auth_setting + ) + + def _apply_auth_params( + self, + headers, + queries, + resource_path, + method, + body, + auth_setting + ) -> None: + """Updates the request parameters based on a single auth_setting + + :param headers: Header parameters dict to be updated. + :param queries: Query parameters tuple list to be updated. + :resource_path: A string representation of the HTTP request resource path. + :method: A string representation of the HTTP request method. + :body: A object representing the body of the HTTP request. + The object type is the return value of sanitize_for_serialization(). + :param auth_setting: auth settings for the endpoint + """ + if auth_setting['in'] == 'cookie': + headers['Cookie'] = auth_setting['value'] + elif auth_setting['in'] == 'header': + if auth_setting['type'] != 'http-signature': + headers[auth_setting['key']] = auth_setting['value'] + elif auth_setting['in'] == 'query': + queries.append((auth_setting['key'], auth_setting['value'])) + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + def __deserialize_file(self, response): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + handle file downloading + save response body into a tmp file and return the instance + + :param response: RESTResponse. + :return: file path. + """ + fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + content_disposition = response.headers.get("Content-Disposition") + if content_disposition: + m = re.search( + r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition + ) + assert m is not None, "Unexpected 'content-disposition' header value" + filename = os.path.basename(m.group(1)) # Strip any directory traversal + if filename in ("", ".", ".."): # fall back to tmp filename + filename = os.path.basename(path) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + f.write(response.data) + + return path + + def __deserialize_primitive(self, data, klass): + """Deserializes string to primitive type. + + :param data: str. + :param klass: class literal. + + :return: int, long, float, str, bool. + """ + try: + return klass(data) + except UnicodeEncodeError: + return str(data) + except TypeError: + return data + + def __deserialize_object(self, value): + """Return an original value. + + :return: object. + """ + return value + + def __deserialize_date(self, string): + """Deserializes string to date. + + :param string: str. + :return: date. + """ + try: + return parse(string).date() + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason="Failed to parse `{0}` as date object".format(string) + ) + + def __deserialize_datetime(self, string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :return: datetime. + """ + try: + return parse(string) + except ImportError: + return string + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as datetime object" + .format(string) + ) + ) + + def __deserialize_enum(self, data, klass): + """Deserializes primitive type to enum. + + :param data: primitive type. + :param klass: class literal. + :return: enum value. + """ + try: + return klass(data) + except ValueError: + raise rest.ApiException( + status=0, + reason=( + "Failed to parse `{0}` as `{1}`" + .format(data, klass) + ) + ) + + def __deserialize_model(self, data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :param klass: class literal. + :return: model object. + """ + + return klass.from_dict(data) diff --git a/regexsolver/generated/api_response.py b/regexsolver/generated/api_response.py new file mode 100644 index 0000000..9bc7c11 --- /dev/null +++ b/regexsolver/generated/api_response.py @@ -0,0 +1,21 @@ +"""API response object.""" + +from __future__ import annotations +from typing import Optional, Generic, Mapping, TypeVar +from pydantic import Field, StrictInt, StrictBytes, BaseModel + +T = TypeVar("T") + +class ApiResponse(BaseModel, Generic[T]): + """ + API response object + """ + + status_code: StrictInt = Field(description="HTTP status code") + headers: Optional[Mapping[str, str]] = Field(None, description="HTTP headers") + data: T = Field(description="Deserialized data given the data type") + raw_data: StrictBytes = Field(description="Raw data (HTTP response body)") + + model_config = { + "arbitrary_types_allowed": True + } diff --git a/regexsolver/generated/configuration.py b/regexsolver/generated/configuration.py new file mode 100644 index 0000000..f385cd4 --- /dev/null +++ b/regexsolver/generated/configuration.py @@ -0,0 +1,581 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import base64 +import copy +import http.client as httplib +import logging +from logging import FileHandler +import sys +from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from typing_extensions import NotRequired, Self + + + +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems' +} + +ServerVariablesT = Dict[str, str] + +GenericAuthSetting = TypedDict( + "GenericAuthSetting", + { + "type": str, + "in": str, + "key": str, + "value": str, + }, +) + + +OAuth2AuthSetting = TypedDict( + "OAuth2AuthSetting", + { + "type": Literal["oauth2"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +APIKeyAuthSetting = TypedDict( + "APIKeyAuthSetting", + { + "type": Literal["api_key"], + "in": str, + "key": str, + "value": Optional[str], + }, +) + + +BasicAuthSetting = TypedDict( + "BasicAuthSetting", + { + "type": Literal["basic"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": Optional[str], + }, +) + + +BearerFormatAuthSetting = TypedDict( + "BearerFormatAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "format": Literal["JWT"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +BearerAuthSetting = TypedDict( + "BearerAuthSetting", + { + "type": Literal["bearer"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": str, + }, +) + + +HTTPSignatureAuthSetting = TypedDict( + "HTTPSignatureAuthSetting", + { + "type": Literal["http-signature"], + "in": Literal["header"], + "key": Literal["Authorization"], + "value": None, + }, +) + + +AuthSettings = TypedDict( + "AuthSettings", + { + "BearerAuth": BearerFormatAuthSetting, + }, + total=False, +) + + +class HostSettingVariable(TypedDict): + description: str + default_value: str + enum_values: List[str] + + +class HostSetting(TypedDict): + url: str + description: str + variables: NotRequired[Dict[str, HostSettingVariable]] + + +class Configuration: + """This class contains various settings of the API client. + + :param host: Base url. + :param ignore_operation_servers + Boolean to ignore operation servers for the API client. + Config will use `host` as the base url regardless of the operation servers. + :param api_key: Dict to store API key(s). + Each entry in the dict specifies an API key. + The dict key is the name of the security scheme in the OAS specification. + The dict value is the API key secret. + :param api_key_prefix: Dict to store API prefix (e.g. Bearer). + The dict key is the name of the security scheme in the OAS specification. + The dict value is an API key prefix when generating the auth data. + :param username: Username for HTTP basic authentication. + :param password: Password for HTTP basic authentication. + :param access_token: Access token. + :param server_index: Index to servers configuration. + :param server_variables: Mapping with string values to replace variables in + templated server configuration. The validation of enums is performed for + variables with defined enum values before. + :param server_operation_index: Mapping from operation ID to an index to server + configuration. + :param server_operation_variables: Mapping from operation ID to a mapping with + string values to replace variables in templated server configuration. + The validation of enums is performed for variables with defined enum + values before. + :param ssl_ca_cert: str - the path to a file of concatenated CA certificates + in PEM format. + :param retries: int | aiohttp_retry.RetryOptionsBase - Retry configuration. + :param ca_cert_data: verify the peer using concatenated CA certificate data + in PEM (str) or DER (bytes) format. + :param cert_file: the path to a client certificate file, for mTLS. + :param key_file: the path to a client key file, for mTLS. + + :Example: + """ + + _default: ClassVar[Optional[Self]] = None + + def __init__( + self, + host: Optional[str]=None, + api_key: Optional[Dict[str, str]]=None, + api_key_prefix: Optional[Dict[str, str]]=None, + username: Optional[str]=None, + password: Optional[str]=None, + access_token: Optional[str]=None, + server_index: Optional[int]=None, + server_variables: Optional[ServerVariablesT]=None, + server_operation_index: Optional[Dict[int, int]]=None, + server_operation_variables: Optional[Dict[int, ServerVariablesT]]=None, + ignore_operation_servers: bool=False, + ssl_ca_cert: Optional[str]=None, + retries: Optional[Union[int, Any]] = None, + ca_cert_data: Optional[Union[str, bytes]] = None, + cert_file: Optional[str]=None, + key_file: Optional[str]=None, + *, + debug: Optional[bool] = None, + ) -> None: + """Constructor + """ + self._base_path = "https://api.regexsolver.com/v1" if host is None else host + """Default Base url + """ + self.server_index = 0 if server_index is None and host is None else server_index + self.server_operation_index = server_operation_index or {} + """Default server index + """ + self.server_variables = server_variables or {} + self.server_operation_variables = server_operation_variables or {} + """Default server variables + """ + self.ignore_operation_servers = ignore_operation_servers + """Ignore operation servers + """ + self.temp_folder_path = None + """Temp file folder for downloading files + """ + # Authentication Settings + self.api_key = {} + if api_key: + self.api_key = api_key + """dict to store API key(s) + """ + self.api_key_prefix = {} + if api_key_prefix: + self.api_key_prefix = api_key_prefix + """dict to store API prefix (e.g. Bearer) + """ + self.refresh_api_key_hook = None + """function hook to refresh API key if expired + """ + self.username = username + """Username for HTTP basic authentication + """ + self.password = password + """Password for HTTP basic authentication + """ + self.access_token = access_token + """Access token + """ + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("regexsolver.generated") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler: Optional[FileHandler] = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + if debug is not None: + self.debug = debug + else: + self.__debug = False + """Debug switch + """ + + self.verify_ssl = True + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = ssl_ca_cert + """Set this to customize the certificate file to verify the peer. + """ + self.ca_cert_data = ca_cert_data + """Set this to verify the peer using PEM (str) or DER (bytes) + certificate data. + """ + self.cert_file = cert_file + """client certificate file + """ + self.key_file = key_file + """client key file + """ + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ + self.tls_server_name = None + """SSL/TLS Server Name Indication (SNI) + Set this to the SNI value expected by the server. + """ + + self.connection_pool_maxsize = 100 + """This value is passed to the aiohttp to limit simultaneous connections. + Default values is 100, None means no-limit. + """ + + self.proxy: Optional[str] = None + """Proxy URL + """ + self.proxy_headers = None + """Proxy headers + """ + self.safe_chars_for_path_param = '' + """Safe chars for path_param + """ + self.retries = retries + """Retry configuration + """ + # Enable client side validation + self.client_side_validation = True + + self.socket_options = None + """Options to pass down to the underlying urllib3 socket + """ + + self.datetime_format = "%Y-%m-%dT%H:%M:%S.%f%z" + """datetime format + """ + + self.date_format = "%Y-%m-%d" + """date format + """ + + def __deepcopy__(self, memo: Dict[int, Any]) -> Self: + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + def __setattr__(self, name: str, value: Any) -> None: + object.__setattr__(self, name, value) + + @classmethod + def set_default(cls, default: Optional[Self]) -> None: + """Set default instance of configuration. + + It stores default configuration, which can be + returned by get_default_copy method. + + :param default: object of Configuration + """ + cls._default = default + + @classmethod + def get_default_copy(cls) -> Self: + """Deprecated. Please use `get_default` instead. + + Deprecated. Please use `get_default` instead. + + :return: The configuration object. + """ + return cls.get_default() + + @classmethod + def get_default(cls) -> Self: + """Return the default configuration. + + This method returns newly created, based on default constructor, + object of Configuration class or returns a copy of default + configuration. + + :return: The configuration object. + """ + if cls._default is None: + cls._default = cls() + return cls._default + + @property + def logger_file(self) -> Optional[str]: + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value: Optional[str]) -> None: + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self) -> bool: + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value: bool) -> None: + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on httplib debug + httplib.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off httplib debug + httplib.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self) -> str: + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value: str) -> None: + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def get_api_key_with_prefix(self, identifier: str, alias: Optional[str]=None) -> Optional[str]: + """Gets API key (with prefix if set). + + :param identifier: The identifier of apiKey. + :param alias: The alternative identifier of apiKey. + :return: The token for api key authentication. + """ + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + if key: + prefix = self.api_key_prefix.get(identifier) + if prefix: + return "%s %s" % (prefix, key) + else: + return key + + return None + + def get_basic_auth_token(self) -> Optional[str]: + """Gets HTTP basic authentication header (string). + + :return: The token for basic HTTP authentication. + """ + username = "" + if self.username is not None: + username = self.username + password = "" + if self.password is not None: + password = self.password + + return "Basic " + base64.b64encode( + (username + ":" + password).encode('utf-8') + ).decode('utf-8') + + def auth_settings(self)-> AuthSettings: + """Gets Auth Settings dict for api client. + + :return: The Auth Settings information dict. + """ + auth: AuthSettings = {} + if self.access_token is not None: + auth['BearerAuth'] = { + 'type': 'bearer', + 'in': 'header', + 'format': 'JWT', + 'key': 'Authorization', + 'value': 'Bearer ' + self.access_token + } + return auth + + def to_debug_report(self) -> str: + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 1.1.0\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_host_settings(self) -> List[HostSetting]: + """Gets an array of host settings + + :return: An array of host settings + """ + return [ + { + 'url': "https://api.regexsolver.com/v1", + 'description': "No description provided", + } + ] + + def get_host_from_settings( + self, + index: Optional[int], + variables: Optional[ServerVariablesT]=None, + servers: Optional[List[HostSetting]]=None, + ) -> str: + """Gets host URL based on the index and variables + :param index: array index of the host settings + :param variables: hash of variable and the corresponding value + :param servers: an array of host settings or None + :return: URL based on host settings + """ + if index is None: + return self._base_path + + variables = {} if variables is None else variables + servers = self.get_host_settings() if servers is None else servers + + try: + server = servers[index] + except IndexError: + raise ValueError( + "Invalid index {0} when selecting the host settings. " + "Must be less than {1}".format(index, len(servers))) + + url = server['url'] + + # go through variables and replace placeholders + for variable_name, variable in server.get('variables', {}).items(): + used_value = variables.get( + variable_name, variable['default_value']) + + if 'enum_values' in variable \ + and variable['enum_values'] \ + and used_value not in variable['enum_values']: + raise ValueError( + "The variable `{0}` in the host URL has invalid value " + "{1}. Must be {2}.".format( + variable_name, variables[variable_name], + variable['enum_values'])) + + url = url.replace("{" + variable_name + "}", used_value) + + return url + + @property + def host(self) -> str: + """Return generated host.""" + return self.get_host_from_settings(self.server_index, variables=self.server_variables) + + @host.setter + def host(self, value: str) -> None: + """Fix base path.""" + self._base_path = value + self.server_index = None diff --git a/regexsolver/generated/exceptions.py b/regexsolver/generated/exceptions.py new file mode 100644 index 0000000..c5e16e9 --- /dev/null +++ b/regexsolver/generated/exceptions.py @@ -0,0 +1,218 @@ +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from typing import Any, Optional +from typing_extensions import Self + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None) -> None: + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None) -> None: + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + + +class ApiException(OpenApiException): + + def __init__( + self, + status=None, + reason=None, + http_resp=None, + *, + body: Optional[str] = None, + data: Optional[Any] = None, + ) -> None: + self.status = status + self.reason = reason + self.body = body + self.data = data + self.headers = None + + if http_resp: + if self.status is None: + self.status = http_resp.status + if self.reason is None: + self.reason = http_resp.reason + if self.body is None: + try: + self.body = http_resp.data.decode('utf-8') + except Exception: + pass + self.headers = http_resp.headers + + @classmethod + def from_response( + cls, + *, + http_resp, + body: Optional[str], + data: Optional[Any], + ) -> Self: + if http_resp.status == 400: + raise BadRequestException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 401: + raise UnauthorizedException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 403: + raise ForbiddenException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 404: + raise NotFoundException(http_resp=http_resp, body=body, data=data) + + # Added new conditions for 409 and 422 + if http_resp.status == 409: + raise ConflictException(http_resp=http_resp, body=body, data=data) + + if http_resp.status == 422: + raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data) + + if 500 <= http_resp.status <= 599: + raise ServiceException(http_resp=http_resp, body=body, data=data) + raise ApiException(http_resp=http_resp, body=body, data=data) + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.headers: + error_message += "HTTP response headers: {0}\n".format( + self.headers) + + if self.body: + error_message += "HTTP response body: {0}\n".format(self.body) + + if self.data: + error_message += "HTTP response data: {0}\n".format(self.data) + + return error_message + + +class BadRequestException(ApiException): + pass + + +class NotFoundException(ApiException): + pass + + +class UnauthorizedException(ApiException): + pass + + +class ForbiddenException(ApiException): + pass + + +class ServiceException(ApiException): + pass + + +class ConflictException(ApiException): + """Exception for HTTP 409 Conflict.""" + pass + + +class UnprocessableEntityException(ApiException): + """Exception for HTTP 422 Unprocessable Entity.""" + pass + + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result diff --git a/regexsolver/generated/models/__init__.py b/regexsolver/generated/models/__init__.py new file mode 100644 index 0000000..aabbb11 --- /dev/null +++ b/regexsolver/generated/models/__init__.py @@ -0,0 +1,42 @@ +# coding: utf-8 + +# flake8: noqa +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +# import models into model package +from regexsolver.generated.models.boolean import Boolean +from regexsolver.generated.models.cardinality import Cardinality +from regexsolver.generated.models.cardinality200_response import Cardinality200Response +from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger +from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite +from regexsolver.generated.models.cardinality_integer import CardinalityInteger +from regexsolver.generated.models.concat200_response import Concat200Response +from regexsolver.generated.models.dot200_response import Dot200Response +from regexsolver.generated.models.empty200_response import Empty200Response +from regexsolver.generated.models.error_response import ErrorResponse +from regexsolver.generated.models.execution_options import ExecutionOptions +from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest +from regexsolver.generated.models.length import Length +from regexsolver.generated.models.length200_response import Length200Response +from regexsolver.generated.models.multi_terms_request import MultiTermsRequest +from regexsolver.generated.models.repeat_request import RepeatRequest +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.response_options import ResponseOptions +from regexsolver.generated.models.string import String +from regexsolver.generated.models.strings import Strings +from regexsolver.generated.models.strings200_response import Strings200Response +from regexsolver.generated.models.term import Term +from regexsolver.generated.models.term_fair import TermFair +from regexsolver.generated.models.term_regex import TermRegex +from regexsolver.generated.models.term_request import TermRequest +from regexsolver.generated.models.two_terms_request import TwoTermsRequest + diff --git a/regexsolver/generated/models/boolean.py b/regexsolver/generated/models/boolean.py new file mode 100644 index 0000000..4aa2a4f --- /dev/null +++ b/regexsolver/generated/models/boolean.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class Boolean(BaseModel): + """ + Wrapper for a boolean value. + """ # noqa: E501 + type: StrictStr + value: StrictBool = Field(description="Boolean value.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['boolean']): + raise ValueError("must be one of enum values ('boolean')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Boolean from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Boolean from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/cardinality.py b/regexsolver/generated/models/cardinality.py new file mode 100644 index 0000000..06276b8 --- /dev/null +++ b/regexsolver/generated/models/cardinality.py @@ -0,0 +1,154 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger +from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite +from regexsolver.generated.models.cardinality_integer import CardinalityInteger +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +CARDINALITY_ONE_OF_SCHEMAS = ["CardinalityBigInteger", "CardinalityInfinite", "CardinalityInteger"] + +class Cardinality(BaseModel): + """ + Number of unique strings matched by a term. + """ + # data type: CardinalityInfinite + oneof_schema_1_validator: Optional[CardinalityInfinite] = None + # data type: CardinalityBigInteger + oneof_schema_2_validator: Optional[CardinalityBigInteger] = None + # data type: CardinalityInteger + oneof_schema_3_validator: Optional[CardinalityInteger] = None + actual_instance: Optional[Union[CardinalityBigInteger, CardinalityInfinite, CardinalityInteger]] = None + one_of_schemas: Set[str] = { "CardinalityBigInteger", "CardinalityInfinite", "CardinalityInteger" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + discriminator_value_class_map: Dict[str, str] = { + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = Cardinality.model_construct() + error_messages = [] + match = 0 + # validate data type: CardinalityInfinite + if not isinstance(v, CardinalityInfinite): + error_messages.append(f"Error! Input type `{type(v)}` is not `CardinalityInfinite`") + else: + match += 1 + # validate data type: CardinalityBigInteger + if not isinstance(v, CardinalityBigInteger): + error_messages.append(f"Error! Input type `{type(v)}` is not `CardinalityBigInteger`") + else: + match += 1 + # validate data type: CardinalityInteger + if not isinstance(v, CardinalityInteger): + error_messages.append(f"Error! Input type `{type(v)}` is not `CardinalityInteger`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in Cardinality with oneOf schemas: CardinalityBigInteger, CardinalityInfinite, CardinalityInteger. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in Cardinality with oneOf schemas: CardinalityBigInteger, CardinalityInfinite, CardinalityInteger. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into CardinalityInfinite + try: + instance.actual_instance = CardinalityInfinite.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into CardinalityBigInteger + try: + instance.actual_instance = CardinalityBigInteger.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into CardinalityInteger + try: + instance.actual_instance = CardinalityInteger.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into Cardinality with oneOf schemas: CardinalityBigInteger, CardinalityInfinite, CardinalityInteger. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into Cardinality with oneOf schemas: CardinalityBigInteger, CardinalityInfinite, CardinalityInteger. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], CardinalityBigInteger, CardinalityInfinite, CardinalityInteger]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regexsolver/generated/models/cardinality200_response.py b/regexsolver/generated/models/cardinality200_response.py new file mode 100644 index 0000000..26dcea0 --- /dev/null +++ b/regexsolver/generated/models/cardinality200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.cardinality import Cardinality +from typing import Optional, Set +from typing_extensions import Self + +class Cardinality200Response(BaseModel): + """ + Cardinality200Response + """ # noqa: E501 + success: StrictBool + data: Cardinality + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Cardinality200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Cardinality200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": Cardinality.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/cardinality_big_integer.py b/regexsolver/generated/models/cardinality_big_integer.py new file mode 100644 index 0000000..0a94022 --- /dev/null +++ b/regexsolver/generated/models/cardinality_big_integer.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CardinalityBigInteger(BaseModel): + """ + The set of matched strings is finite but too large to be returned. + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['bigInteger']): + raise ValueError("must be one of enum values ('bigInteger')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CardinalityBigInteger from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CardinalityBigInteger from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regexsolver/generated/models/cardinality_infinite.py b/regexsolver/generated/models/cardinality_infinite.py new file mode 100644 index 0000000..9db9135 --- /dev/null +++ b/regexsolver/generated/models/cardinality_infinite.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class CardinalityInfinite(BaseModel): + """ + The set of matched strings is infinite. + """ # noqa: E501 + type: StrictStr + __properties: ClassVar[List[str]] = ["type"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['infinite']): + raise ValueError("must be one of enum values ('infinite')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CardinalityInfinite from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CardinalityInfinite from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type") + }) + return _obj + + diff --git a/regexsolver/generated/models/cardinality_integer.py b/regexsolver/generated/models/cardinality_integer.py new file mode 100644 index 0000000..9952680 --- /dev/null +++ b/regexsolver/generated/models/cardinality_integer.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class CardinalityInteger(BaseModel): + """ + The set of matched strings is finite. + """ # noqa: E501 + type: StrictStr + value: Annotated[int, Field(strict=True, ge=0)] = Field(description="Exact count.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['integer']): + raise ValueError("must be one of enum values ('integer')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of CardinalityInteger from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of CardinalityInteger from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/concat200_response.py b/regexsolver/generated/models/concat200_response.py new file mode 100644 index 0000000..2e9288a --- /dev/null +++ b/regexsolver/generated/models/concat200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class Concat200Response(BaseModel): + """ + Concat200Response + """ # noqa: E501 + success: StrictBool + data: Term + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Concat200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Concat200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": Term.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/dot200_response.py b/regexsolver/generated/models/dot200_response.py new file mode 100644 index 0000000..095b5a2 --- /dev/null +++ b/regexsolver/generated/models/dot200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.string import String +from typing import Optional, Set +from typing_extensions import Self + +class Dot200Response(BaseModel): + """ + Dot200Response + """ # noqa: E501 + success: StrictBool + data: String + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Dot200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Dot200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": String.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/empty200_response.py b/regexsolver/generated/models/empty200_response.py new file mode 100644 index 0000000..0539535 --- /dev/null +++ b/regexsolver/generated/models/empty200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.boolean import Boolean +from typing import Optional, Set +from typing_extensions import Self + +class Empty200Response(BaseModel): + """ + Empty200Response + """ # noqa: E501 + success: StrictBool + data: Boolean + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Empty200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Empty200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": Boolean.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/error_response.py b/regexsolver/generated/models/error_response.py new file mode 100644 index 0000000..bd9a5d1 --- /dev/null +++ b/regexsolver/generated/models/error_response.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class ErrorResponse(BaseModel): + """ + Standard error payload returned when success is false. + """ # noqa: E501 + success: StrictBool + error: StrictStr = Field(description="Human readable error message.") + __properties: ClassVar[List[str]] = ["success", "error"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ErrorResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ErrorResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "error": obj.get("error") + }) + return _obj + + diff --git a/regexsolver/generated/models/execution_options.py b/regexsolver/generated/models/execution_options.py new file mode 100644 index 0000000..e75b7ec --- /dev/null +++ b/regexsolver/generated/models/execution_options.py @@ -0,0 +1,88 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from typing import Optional, Set +from typing_extensions import Self + +class ExecutionOptions(BaseModel): + """ + Change how the engine executes the operation. + """ # noqa: E501 + timeout: Optional[Annotated[int, Field(strict=True, ge=1)]] = Field(default=None, description="Timeout in milliseconds for the operation.") + __properties: ClassVar[List[str]] = ["timeout"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ExecutionOptions from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ExecutionOptions from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "timeout": obj.get("timeout") + }) + return _obj + + diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/generated/models/generate_strings_request.py new file mode 100644 index 0000000..ae8862d --- /dev/null +++ b/regexsolver/generated/models/generate_strings_request.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class GenerateStringsRequest(BaseModel): + """ + Request to generate up to 'count' distinct strings matched by 'term'. + """ # noqa: E501 + term: Term = Field(description="Source term to sample from.") + count: StrictInt = Field(description="Maximum number of unique strings to return.") + options: Optional[RequestOptions] = None + __properties: ClassVar[List[str]] = ["term", "count", "options"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GenerateStringsRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + # override the default output from pydantic by calling `to_dict()` of options + if self.options: + _dict['options'] = self.options.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GenerateStringsRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "count": obj.get("count"), + "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/length.py b/regexsolver/generated/models/length.py new file mode 100644 index 0000000..51de676 --- /dev/null +++ b/regexsolver/generated/models/length.py @@ -0,0 +1,108 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class Length(BaseModel): + """ + Minimum and maximum length of any string in the language. + """ # noqa: E501 + type: StrictStr + min: Optional[StrictInt] = Field(description="Shortest possible length, or null if empty.") + max: Optional[StrictInt] = Field(description="Longest possible length, or null if unbounded.") + __properties: ClassVar[List[str]] = ["type", "min", "max"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['length']): + raise ValueError("must be one of enum values ('length')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Length from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # set to None if min (nullable) is None + # and model_fields_set contains the field + if self.min is None and "min" in self.model_fields_set: + _dict['min'] = None + + # set to None if max (nullable) is None + # and model_fields_set contains the field + if self.max is None and "max" in self.model_fields_set: + _dict['max'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Length from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "min": obj.get("min"), + "max": obj.get("max") + }) + return _obj + + diff --git a/regexsolver/generated/models/length200_response.py b/regexsolver/generated/models/length200_response.py new file mode 100644 index 0000000..66fed08 --- /dev/null +++ b/regexsolver/generated/models/length200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.length import Length +from typing import Optional, Set +from typing_extensions import Self + +class Length200Response(BaseModel): + """ + Length200Response + """ # noqa: E501 + success: StrictBool + data: Length + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Length200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Length200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": Length.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/multi_terms_request.py b/regexsolver/generated/models/multi_terms_request.py new file mode 100644 index 0000000..3e96cb0 --- /dev/null +++ b/regexsolver/generated/models/multi_terms_request.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class MultiTermsRequest(BaseModel): + """ + Request carrying 2 or more terms for n-ary operations. + """ # noqa: E501 + terms: Annotated[List[Term], Field(min_length=2)] = Field(description="Terms to process. Order matters for some operations.") + options: Optional[RequestOptions] = None + __properties: ClassVar[List[str]] = ["terms", "options"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of MultiTermsRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in terms (list) + _items = [] + if self.terms: + for _item_terms in self.terms: + if _item_terms: + _items.append(_item_terms.to_dict()) + _dict['terms'] = _items + # override the default output from pydantic by calling `to_dict()` of options + if self.options: + _dict['options'] = self.options.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of MultiTermsRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "terms": [Term.from_dict(_item) for _item in obj["terms"]] if obj.get("terms") is not None else None, + "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/repeat_request.py b/regexsolver/generated/models/repeat_request.py new file mode 100644 index 0000000..6c78186 --- /dev/null +++ b/regexsolver/generated/models/repeat_request.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class RepeatRequest(BaseModel): + """ + Request to repeat a term between 'min' and 'max' times. + """ # noqa: E501 + term: Term = Field(description="Term to repeat.") + min: StrictInt = Field(description="Inclusive lower bound of repetitions.") + max: Optional[StrictInt] = Field(default=None, description="Inclusive upper bound. If omitted or null, the repetition is unbounded.") + options: Optional[RequestOptions] = None + __properties: ClassVar[List[str]] = ["term", "min", "max", "options"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RepeatRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + # override the default output from pydantic by calling `to_dict()` of options + if self.options: + _dict['options'] = self.options.to_dict() + # set to None if max (nullable) is None + # and model_fields_set contains the field + if self.max is None and "max" in self.model_fields_set: + _dict['max'] = None + + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RepeatRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "min": obj.get("min"), + "max": obj.get("max"), + "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/request_options.py b/regexsolver/generated/models/request_options.py new file mode 100644 index 0000000..883cfff --- /dev/null +++ b/regexsolver/generated/models/request_options.py @@ -0,0 +1,99 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver.generated.models.execution_options import ExecutionOptions +from regexsolver.generated.models.response_options import ResponseOptions +from typing import Optional, Set +from typing_extensions import Self + +class RequestOptions(BaseModel): + """ + Change how the engine handle the operation. + """ # noqa: E501 + schema_version: StrictInt = Field(description="Client-expected schema version.", alias="schemaVersion") + response: Optional[ResponseOptions] = None + execution: Optional[ExecutionOptions] = None + __properties: ClassVar[List[str]] = ["schemaVersion", "response", "execution"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of RequestOptions from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of response + if self.response: + _dict['response'] = self.response.to_dict() + # override the default output from pydantic by calling `to_dict()` of execution + if self.execution: + _dict['execution'] = self.execution.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of RequestOptions from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "schemaVersion": obj.get("schemaVersion"), + "response": ResponseOptions.from_dict(obj["response"]) if obj.get("response") is not None else None, + "execution": ExecutionOptions.from_dict(obj["execution"]) if obj.get("execution") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/response_options.py b/regexsolver/generated/models/response_options.py new file mode 100644 index 0000000..24206e9 --- /dev/null +++ b/regexsolver/generated/models/response_options.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class ResponseOptions(BaseModel): + """ + Change how the engine returns results. + """ # noqa: E501 + format: Optional[StrictStr] = Field(default=None, description="Return format of the term.") + __properties: ClassVar[List[str]] = ["format"] + + @field_validator('format') + def format_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(['any', 'fair', 'regex']): + raise ValueError("must be one of enum values ('any', 'fair', 'regex')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ResponseOptions from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ResponseOptions from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "format": obj.get("format") + }) + return _obj + + diff --git a/regexsolver/generated/models/string.py b/regexsolver/generated/models/string.py new file mode 100644 index 0000000..c003aa4 --- /dev/null +++ b/regexsolver/generated/models/string.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class String(BaseModel): + """ + Wrapper for a string value. + """ # noqa: E501 + type: StrictStr + value: StrictStr = Field(description="String value.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['string']): + raise ValueError("must be one of enum values ('string')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of String from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of String from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/strings.py b/regexsolver/generated/models/strings.py new file mode 100644 index 0000000..e69551c --- /dev/null +++ b/regexsolver/generated/models/strings.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class Strings(BaseModel): + """ + Wrapper for a list of strings. + """ # noqa: E501 + type: StrictStr + value: List[StrictStr] = Field(description="Array of unique strings.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['strings']): + raise ValueError("must be one of enum values ('strings')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Strings from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Strings from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/strings200_response.py b/regexsolver/generated/models/strings200_response.py new file mode 100644 index 0000000..2290655 --- /dev/null +++ b/regexsolver/generated/models/strings200_response.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver.generated.models.strings import Strings +from typing import Optional, Set +from typing_extensions import Self + +class Strings200Response(BaseModel): + """ + Strings200Response + """ # noqa: E501 + success: StrictBool + data: Strings + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Strings200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Strings200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": Strings.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/term.py b/regexsolver/generated/models/term.py new file mode 100644 index 0000000..4575d24 --- /dev/null +++ b/regexsolver/generated/models/term.py @@ -0,0 +1,140 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +import pprint +from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator +from typing import Any, List, Optional +from regexsolver.generated.models.term_fair import TermFair +from regexsolver.generated.models.term_regex import TermRegex +from pydantic import StrictStr, Field +from typing import Union, List, Set, Optional, Dict +from typing_extensions import Literal, Self + +TERM_ONE_OF_SCHEMAS = ["TermFair", "TermRegex"] + +class Term(BaseModel): + """ + Serialized term. + """ + # data type: TermRegex + oneof_schema_1_validator: Optional[TermRegex] = None + # data type: TermFair + oneof_schema_2_validator: Optional[TermFair] = None + actual_instance: Optional[Union[TermFair, TermRegex]] = None + one_of_schemas: Set[str] = { "TermFair", "TermRegex" } + + model_config = ConfigDict( + validate_assignment=True, + protected_namespaces=(), + ) + + + discriminator_value_class_map: Dict[str, str] = { + } + + def __init__(self, *args, **kwargs) -> None: + if args: + if len(args) > 1: + raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`") + if kwargs: + raise ValueError("If a position argument is used, keyword arguments cannot be used.") + super().__init__(actual_instance=args[0]) + else: + super().__init__(**kwargs) + + @field_validator('actual_instance') + def actual_instance_must_validate_oneof(cls, v): + instance = Term.model_construct() + error_messages = [] + match = 0 + # validate data type: TermRegex + if not isinstance(v, TermRegex): + error_messages.append(f"Error! Input type `{type(v)}` is not `TermRegex`") + else: + match += 1 + # validate data type: TermFair + if not isinstance(v, TermFair): + error_messages.append(f"Error! Input type `{type(v)}` is not `TermFair`") + else: + match += 1 + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when setting `actual_instance` in Term with oneOf schemas: TermFair, TermRegex. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when setting `actual_instance` in Term with oneOf schemas: TermFair, TermRegex. Details: " + ", ".join(error_messages)) + else: + return v + + @classmethod + def from_dict(cls, obj: Union[str, Dict[str, Any]]) -> Self: + return cls.from_json(json.dumps(obj)) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Returns the object represented by the json string""" + instance = cls.model_construct() + error_messages = [] + match = 0 + + # deserialize data into TermRegex + try: + instance.actual_instance = TermRegex.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + # deserialize data into TermFair + try: + instance.actual_instance = TermFair.from_json(json_str) + match += 1 + except (ValidationError, ValueError) as e: + error_messages.append(str(e)) + + if match > 1: + # more than 1 match + raise ValueError("Multiple matches found when deserializing the JSON string into Term with oneOf schemas: TermFair, TermRegex. Details: " + ", ".join(error_messages)) + elif match == 0: + # no match + raise ValueError("No match found when deserializing the JSON string into Term with oneOf schemas: TermFair, TermRegex. Details: " + ", ".join(error_messages)) + else: + return instance + + def to_json(self) -> str: + """Returns the JSON representation of the actual instance""" + if self.actual_instance is None: + return "null" + + if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json): + return self.actual_instance.to_json() + else: + return json.dumps(self.actual_instance) + + def to_dict(self) -> Optional[Union[Dict[str, Any], TermFair, TermRegex]]: + """Returns the dict representation of the actual instance""" + if self.actual_instance is None: + return None + + if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict): + return self.actual_instance.to_dict() + else: + # primitive type + return self.actual_instance + + def to_str(self) -> str: + """Returns the string representation of the actual instance""" + return pprint.pformat(self.model_dump()) + + diff --git a/regexsolver/generated/models/term_fair.py b/regexsolver/generated/models/term_fair.py new file mode 100644 index 0000000..d925100 --- /dev/null +++ b/regexsolver/generated/models/term_fair.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class TermFair(BaseModel): + """ + Term encoded as FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine. + """ # noqa: E501 + type: StrictStr + value: StrictStr = Field(description="FAIR payload.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['fair']): + raise ValueError("must be one of enum values ('fair')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TermFair from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TermFair from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/term_regex.py b/regexsolver/generated/models/term_regex.py new file mode 100644 index 0000000..892cbc7 --- /dev/null +++ b/regexsolver/generated/models/term_regex.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self + +class TermRegex(BaseModel): + """ + Term encoded as a regular expression pattern. + """ # noqa: E501 + type: StrictStr + value: StrictStr = Field(description="Regular expression pattern.") + __properties: ClassVar[List[str]] = ["type", "value"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['regex']): + raise ValueError("must be one of enum values ('regex')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TermRegex from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TermRegex from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "value": obj.get("value") + }) + return _obj + + diff --git a/regexsolver/generated/models/term_request.py b/regexsolver/generated/models/term_request.py new file mode 100644 index 0000000..ef3605b --- /dev/null +++ b/regexsolver/generated/models/term_request.py @@ -0,0 +1,97 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class TermRequest(BaseModel): + """ + Request a single term. + """ # noqa: E501 + term: Term + options: Optional[RequestOptions] = None + __properties: ClassVar[List[str]] = ["term", "options"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TermRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + # override the default output from pydantic by calling `to_dict()` of options + if self.options: + _dict['options'] = self.options.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TermRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/two_terms_request.py b/regexsolver/generated/models/two_terms_request.py new file mode 100644 index 0000000..a422a8d --- /dev/null +++ b/regexsolver/generated/models/two_terms_request.py @@ -0,0 +1,102 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field +from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated +from regexsolver.generated.models.request_options import RequestOptions +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class TwoTermsRequest(BaseModel): + """ + Request carrying exactly 2 terms. + """ # noqa: E501 + terms: Annotated[List[Term], Field(min_length=2, max_length=2)] = Field(description="Exactly 2 terms.") + options: Optional[RequestOptions] = None + __properties: ClassVar[List[str]] = ["terms", "options"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TwoTermsRequest from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in terms (list) + _items = [] + if self.terms: + for _item_terms in self.terms: + if _item_terms: + _items.append(_item_terms.to_dict()) + _dict['terms'] = _items + # override the default output from pydantic by calling `to_dict()` of options + if self.options: + _dict['options'] = self.options.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TwoTermsRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "terms": [Term.from_dict(_item) for _item in obj["terms"]] if obj.get("terms") is not None else None, + "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/py.typed b/regexsolver/generated/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/regexsolver/generated/rest.py b/regexsolver/generated/rest.py new file mode 100644 index 0000000..2db6fca --- /dev/null +++ b/regexsolver/generated/rest.py @@ -0,0 +1,226 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import io +import json +import re +import ssl +from typing import Optional, Union + +import aiohttp +import aiohttp_retry + +from regexsolver.generated.exceptions import ApiException, ApiValueError + +RESTResponseType = aiohttp.ClientResponse + +ALLOW_RETRY_METHODS = frozenset({'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'TRACE'}) + +class RESTResponse(io.IOBase): + + def __init__(self, resp) -> None: + self.response = resp + self.status = resp.status + self.reason = resp.reason + self.data = None + + async def read(self): + if self.data is None: + self.data = await self.response.read() + return self.data + + @property + def headers(self): + """Returns a CIMultiDictProxy of response headers.""" + return self.response.headers + + def getheaders(self): + """Returns a CIMultiDictProxy of the response headers; use ``headers`` instead.""" + return self.response.headers + + def getheader(self, name, default=None): + """Returns a given response header; use ``headers.get()`` instead.""" + return self.response.headers.get(name, default) + + +class RESTClientObject: + + def __init__(self, configuration) -> None: + + # maxsize is number of requests to host that are allowed in parallel + self.maxsize = configuration.connection_pool_maxsize + + self.ssl_context = ssl.create_default_context( + cafile=configuration.ssl_ca_cert, + cadata=configuration.ca_cert_data, + ) + if configuration.cert_file: + self.ssl_context.load_cert_chain( + configuration.cert_file, keyfile=configuration.key_file + ) + + if not configuration.verify_ssl: + self.ssl_context.check_hostname = False + self.ssl_context.verify_mode = ssl.CERT_NONE + + self.proxy = configuration.proxy + self.proxy_headers = configuration.proxy_headers + + retries = configuration.retries + if retries is None: + self._effective_retry_options = None + elif isinstance(retries, aiohttp_retry.RetryOptionsBase): + self._effective_retry_options = retries + elif isinstance(retries, int): + self._effective_retry_options = aiohttp_retry.ExponentialRetry( + attempts=retries, + factor=2.0, + start_timeout=0.1, + max_timeout=120.0 + ) + else: + self._effective_retry_options = None + + self.pool_manager: Optional[aiohttp.ClientSession] = None + self.retry_client: Optional[aiohttp_retry.RetryClient] = None + + async def close(self) -> None: + if self.pool_manager: + await self.pool_manager.close() + if self.retry_client is not None: + await self.retry_client.close() + + async def request( + self, + method, + url, + headers=None, + body=None, + post_params=None, + _request_timeout=None + ): + """Execute request + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request json body, for `application/json` + :param post_params: request post parameters, + `application/x-www-form-urlencoded` + and `multipart/form-data` + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + method = method.upper() + assert method in [ + 'GET', + 'HEAD', + 'DELETE', + 'POST', + 'PUT', + 'PATCH', + 'OPTIONS' + ] + + if post_params and body: + raise ApiValueError( + "body parameter cannot be used with post_params parameter." + ) + + post_params = post_params or {} + headers = headers or {} + # url already contains the URL query string + timeout = _request_timeout or 5 * 60 + + if 'Content-Type' not in headers: + headers['Content-Type'] = 'application/json' + + args = { + "method": method, + "url": url, + "timeout": timeout, + "headers": headers + } + + if self.proxy: + args["proxy"] = self.proxy + if self.proxy_headers: + args["proxy_headers"] = self.proxy_headers + + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: + if re.search('json', headers['Content-Type'], re.IGNORECASE): + if body is not None: + body = json.dumps(body) + args["data"] = body + elif headers['Content-Type'] == 'application/x-www-form-urlencoded': + args["data"] = aiohttp.FormData(post_params) + elif headers['Content-Type'] == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by aiohttp + del headers['Content-Type'] + data = aiohttp.FormData() + for param in post_params: + k, v = param + if isinstance(v, tuple) and len(v) == 3: + data.add_field( + k, + value=v[1], + filename=v[0], + content_type=v[2] + ) + else: + # Ensures that dict objects are serialized + if isinstance(v, dict): + v = json.dumps(v) + elif isinstance(v, int): + v = str(v) + data.add_field(k, v) + args["data"] = data + + # Pass a `bytes` or `str` parameter directly in the body to support + # other content types than Json when `body` argument is provided + # in serialized form + elif isinstance(body, str) or isinstance(body, bytes): + args["data"] = body + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise ApiException(status=0, reason=msg) + + pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient] + + # https pool manager + if self.pool_manager is None: + self.pool_manager = aiohttp.ClientSession( + connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context), + trust_env=True, + ) + pool_manager = self.pool_manager + + if self._effective_retry_options is not None and method in ALLOW_RETRY_METHODS: + if self.retry_client is None: + self.retry_client = aiohttp_retry.RetryClient( + client_session=self.pool_manager, + retry_options=self._effective_retry_options + ) + pool_manager = self.retry_client + + r = await pool_manager.request(**args) + + return RESTResponse(r) diff --git a/regexsolver/models/cardinality.py b/regexsolver/models/cardinality.py new file mode 100644 index 0000000..cee6c7e --- /dev/null +++ b/regexsolver/models/cardinality.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass +from typing import Optional + +from regexsolver.models.term_properties_mixin import TermPropertiesMixin + + +class Cardinality(TermPropertiesMixin): + """Base class representing the number of unique strings matched by a term.""" + + pass + + +@dataclass(frozen=True) +class Infinite(Cardinality): + """Indicates that the set of matched strings is infinite.""" + + def is_empty(self) -> Optional[bool]: + return False + + def is_empty_string(self) -> Optional[bool]: + return False + + +@dataclass(frozen=True) +class BigInteger(Cardinality): + """Indicates that the set of matched strings is finite but too large to be returned as a standard integer.""" + + def is_empty(self) -> Optional[bool]: + return False + + def is_empty_string(self) -> Optional[bool]: + return False + + def is_total(self) -> Optional[bool]: + return False + + +@dataclass(frozen=True) +class Integer(Cardinality): + """Indicates that the set of matched strings is finite and exactly calculable. + + Attributes: + value (int): The exact count of uniquely matched strings. + """ + + value: int + + def is_empty(self) -> bool: + return self.value == 0 + + def is_empty_string(self) -> Optional[bool]: + if self.value == 1: + return None + return False + + def is_total(self) -> Optional[bool]: + return False diff --git a/regexsolver/models/length.py b/regexsolver/models/length.py new file mode 100644 index 0000000..3dc5f9f --- /dev/null +++ b/regexsolver/models/length.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import Optional + +from regexsolver.models.term_properties_mixin import TermPropertiesMixin + + +@dataclass +class Length(TermPropertiesMixin): + """Represents the minimum and maximum lengths of any string matched by the term. + + Attributes: + min (Optional[int]): The shortest possible matched string length, or None if the language is empty. + max (Optional[int]): The longest possible matched string length, or None if the length is unbounded. + """ + + min: Optional[int] + max: Optional[int] + + def __repr__(self) -> str: + return f"" + + def is_empty(self) -> Optional[bool]: + return self.min is None and self.max is None + + def is_empty_string(self) -> Optional[bool]: + return self.min == 0 and self.max == 0 + + def is_total(self) -> Optional[bool]: + if self.min != 0 or self.max is not None: + return False + else: + return None diff --git a/regexsolver/models/response_format.py b/regexsolver/models/response_format.py new file mode 100644 index 0000000..0d97f39 --- /dev/null +++ b/regexsolver/models/response_format.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class ResponseFormat(str, Enum): + """Defines the format in which the engine should return computed Terms. + + Attributes: + ANY: Allows the engine to return the result in the most efficient format. + FAIR: Fast Automaton Internal Representation, a stable internal format. + REGEX: Standard regular expression pattern. + """ + + ANY = "any" + FAIR = "fair" + REGEX = "regex" + + def __str__(self) -> str: + return str(self.value) diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py new file mode 100644 index 0000000..7d9500a --- /dev/null +++ b/regexsolver/models/term.py @@ -0,0 +1,144 @@ +from typing import Optional, Union, cast + +from regexsolver.generated.models import Term as GeneratedTerm +from regexsolver.generated.models.term_fair import TermFair +from regexsolver.generated.models.term_regex import TermRegex +from regexsolver.models.cardinality import Cardinality +from regexsolver.models.length import Length +from regexsolver.models.term_properties_mixin import TermPropertiesMixin + + +class Term: + """Represents a mathematical term (Regex or FAIR) on which operations can be performed. + + This is a pure data model that holds the underlying pattern and caches + computed properties (like length and cardinality) to minimize network I/O. + """ + + def __init__(self, generated_term: GeneratedTerm): + """Initializes a new Term instance wrapping the generated API model. + + Args: + generated_term (GeneratedTerm): The raw Pydantic model generated by OpenAPI. + """ + self._inner_term = generated_term + + self._cardinality: Optional[Cardinality] = None + self._length: Optional[Length] = None + self._empty: Optional[bool] = None + self._empty_string: Optional[bool] = None + self._total: Optional[bool] = None + self._pattern: Optional[str] = None + self._dot: Optional[str] = None + + @property + def _actual_model(self) -> Union[TermRegex, TermFair]: + """Extracts the underlying concrete model from the generated OpenAPI oneOf wrapper.""" + actual = getattr(self._inner_term, "actual_instance", self._inner_term) + return cast(Union[TermRegex, TermFair], actual) + + def _set_properties_mixin(self, properties_mixin: TermPropertiesMixin): + """Updates internal cached properties based on trait inferences.""" + empty = properties_mixin.is_empty() + if empty is not None: + self._empty = empty + + empty_string = properties_mixin.is_empty_string() + if empty_string is not None: + self._empty_string = empty_string + + total = properties_mixin.is_total() + if total is not None: + self._total = total + + @property + def type(self) -> str: + """str: The format type of the underlying term (e.g., 'regex' or 'fair').""" + return self._actual_model.type + + @property + def value(self) -> str: + """str: The raw string value of the term (the pattern or FAIR payload).""" + return self._actual_model.value + + @property + def _api_model(self) -> GeneratedTerm: + """GeneratedTerm: Returns the raw generated model to be used in API requests.""" + return self._inner_term + + @classmethod + def fair(cls, fair: str) -> "Term": + """Creates a new Term from a Fast Automaton Internal Representation (FAIR) string. + + Args: + fair: The FAIR encoded payload. + + Returns: + Term: A new Term instance representing the FAIR payload. + """ + gen_term = GeneratedTerm(TermFair(type="regex", value=fair)) + return cls(gen_term) + + @classmethod + def regex(cls, pattern: str) -> "Term": + """Creates a new Term from a regular expression string. + + Args: + pattern: A valid regular expression string. + + Returns: + Term: A new Term instance representing the regex. + """ + gen_term = GeneratedTerm(TermRegex(type="regex", value=pattern)) + return cls(gen_term) + + def get_fair(self) -> Optional[str]: + """Retrieves the FAIR payload if the term was explicitly constructed as one. + + Returns: + Optional[str]: The FAIR payload string, or None if the term is a standard regex. + """ + if self.type == "fair": + return self.value + return None + + def serialize(self) -> str: + """Serializes the Term into a portable string format. + + Returns: + str: The serialized string in the format 'type=value' (e.g., 'regex=[a-z]'). + """ + actual_model = self._actual_model + return f"{actual_model.type}={actual_model.value}" + + @staticmethod + def deserialize(string: str) -> Optional["Term"]: + """Reconstructs a Term instance from a serialized string. + + Args: + string: A string previously generated by `Term.serialize()`. + + Returns: + Optional[Term]: The parsed Term instance, or None if parsing fails. + """ + if not string or "=" not in string: + return None + prefix, value = string.split("=", 1) + if prefix == "regex": + return Term.regex(value) + elif prefix == "fair": + return Term.fair(value) + return None + + def __str__(self) -> str: + """Returns the serialized representation of the Term.""" + return self.serialize() + + def __eq__(self, other: object) -> bool: + if isinstance(other, Term): + return self.type == other.type and self.value == other.value + return False + + def __hash__(self) -> int: + """Generates a hash based on the serialized string representation.""" + return hash(self.serialize()) diff --git a/regexsolver/models/term_properties_mixin.py b/regexsolver/models/term_properties_mixin.py new file mode 100644 index 0000000..4752546 --- /dev/null +++ b/regexsolver/models/term_properties_mixin.py @@ -0,0 +1,32 @@ +from typing import Optional + + +class TermPropertiesMixin: + """A mixin providing default property inference for Term analytics. + + Returns `None` when a property cannot be strictly inferred from the current data alone. + """ + + def is_empty(self) -> Optional[bool]: + """Infers whether the term matches no strings at all. + + Returns: + Optional[bool]: True if it definitely matches no strings, False if it matches at least one, or None if it cannot be inferred. + """ + return None + + def is_empty_string(self) -> Optional[bool]: + """Infers whether the term matches strictly the empty string (""). + + Returns: + Optional[bool]: True if it definitely matches only the empty string, False if it matches other strings, or None if it cannot be inferred. + """ + return None + + def is_total(self) -> Optional[bool]: + """Infers whether the term matches all possible strings. + + Returns: + Optional[bool]: True if it definitely matches all strings, False if it misses at least one string, or None if it cannot be inferred. + """ + return None diff --git a/requirements.txt b/requirements.txt index 63b3919..8c5c440 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -requests>=2.20.0 -pydantic<=2.5.3, >2.4.0; python_version<"3.8" -pydantic>=2.6.0; python_version>="3.8" \ No newline at end of file +python_dateutil >= 2.8.2 +aiohttp >= 3.8.4 +aiohttp-retry >= 2.8.3 +pydantic >= 2 +typing-extensions >= 4.7.1 diff --git a/setup.py b/setup.py index 9db4e61..fa5df4d 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,36 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="regexsolver", version="1.1.0", description="RegexSolver is a powerful toolkit for building, combining, and analyzing regular expressions.", - long_description=open('README.md').read(), - long_description_content_type='text/markdown', + long_description=open("README.md").read(), + long_description_content_type="text/markdown", author="RegexSolver", author_email="contact@regexsolver.com", url="https://github.com/RegexSolver/regexsolver-python", license="MIT", keywords="regex regexp pattern intersection union difference concat equivalence subset nfa dfa", packages=find_packages(exclude=["tests", "tests.*"]), - install_requires=[ - 'requests>=2.20.0', - 'pydantic<=2.5.3, >2.4.0; python_version<"3.8"', - 'pydantic>=2.6.0; python_version>="3.8"' + "aiohttp>=3.8.4", + "aiohttp-retry>=2.8.3", + "python-dateutil>=2.8.2", + "pydantic>=2.0.0", + "typing-extensions>=4.7.1", ], - python_requires='>=3.7', + extras_require={ + "test": [ + "pytest>=7.2.1", + "pytest-cov>=2.8.1", + "pytest-asyncio>=1.3.0", + "tox>=3.9.0", + "flake8>=4.0.0", + "mypy>=1.5", + "types-python-dateutil>=2.8.19.14", + ] + }, + python_requires=">=3.9", project_urls={ "Homepage": "https://regexsolver.com/", "Issues": "https://github.com/RegexSolver/regexsolver-python/issues", @@ -30,8 +42,6 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", diff --git a/test-requirements.txt b/test-requirements.txt index 606d9d3..3104aef 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,7 @@ -requests_mock>=1.9.0 -python-dotenv==1.1.1 \ No newline at end of file +pytest >= 7.2.1 +pytest-cov >= 2.8.1 +tox >= 3.9.0 +flake8 >= 4.0.0 +types-python-dateutil >= 2.8.19.14 +mypy >= 1.5 +pytest-asyncio >= 1.3.0 diff --git a/tests/assets/response_error.json b/tests/assets/response_error.json deleted file mode 100644 index 0faf2d7..0000000 --- a/tests/assets/response_error.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "error", - "message": "A random error." -} \ No newline at end of file diff --git a/tests/integration_test.py b/tests/integration_test.py deleted file mode 100644 index c601c19..0000000 --- a/tests/integration_test.py +++ /dev/null @@ -1,173 +0,0 @@ -import unittest -from dotenv import load_dotenv -from regexsolver import RegexSolver, ResponseFormat, Term - - -class IntegrationTest(unittest.TestCase): - def setUp(self): - load_dotenv() - RegexSolver.initialize() - - # Analyze - - def test_analyze_cardinality(self): - term = Term.regex(r"[0-4]") - cardinality = term.get_cardinality() - - self.assertEqual( - "Integer(5)", - str(cardinality) - ) - - def test_analyze_dot(self): - term = Term.regex(r"(abc|de)") - dot = term.get_dot() - - self.assertTrue(dot.startswith("digraph ")) - - def test_analyze_empty_string(self): - term = Term.regex(r"") - - result = term.is_empty_string() - - self.assertTrue(result) - - def test_analyze_empty(self): - term = Term.regex(r"[]") - - result = term.is_empty() - - self.assertTrue(result) - - def test_analyze_total(self): - term = Term.regex(r".*") - - result = term.is_total() - - self.assertTrue(result) - - def test_analyze_equivalent(self): - term1 = Term.regex(r"(abc|de)") - term2 = Term.fair("sLc#w-!No&(opHq@B-9o[LpP-a#fYI+" - )) - self.assert_serialization(Term.fair("=rgmsW[1g2LvP=Gr&+")) - self.assert_serialization(Term.fair("")) - - def assert_serialization(self, term: Term): - serialized = term.serialize() - deserialized = Term.deserialize(serialized) - - self.assertEqual(term, deserialized) - - def test_serialize_requests(self): - request = MultiTermsRequest( - terms=[Term.regex(r"abc"), Term.regex(r"def"), Term.regex(r"ghi")]) - self.assertEqual( - { - "terms": [ - {"type": "regex", "value": "abc"}, - {"type": "regex", "value": "def"}, - {"type": "regex", "value": "ghi"} - ] - }, - request.model_dump(exclude_none=True) - ) - - request = MultiTermsRequest( - terms=[Term.regex(r"abc"), Term.regex(r"def"), Term.regex(r"ghi")], - options=RequestOptions.from_args(response_format=ResponseFormat.FAIR, execution_timeout=400) - ) - self.assertEqual( - { - "terms": [ - {"type": "regex", "value": "abc"}, - {"type": "regex", "value": "def"}, - {"type": "regex", "value": "ghi"} - ], - "options": { - "schema_version": 1, - "response": { - "format": "fair" - }, - "execution": { - "timeout": 400 - } - } - }, - request.model_dump(exclude_none=True) - ) - - request = GenerateStringsRequest( - term=Term.regex(r"(abc|de){2,3}"), count=10) - self.assertEqual( - { - "term": {"type": "regex", "value": "(abc|de){2,3}"}, - "count": 10 - }, - request.model_dump(exclude_none=True) - ) - - request = Term.regex(r"(abc|de){2,3}") - self.assertEqual( - {"type": "regex", "value": "(abc|de){2,3}"}, - request.model_dump() - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/term_operation_test.py b/tests/term_operation_test.py deleted file mode 100644 index bae8bb6..0000000 --- a/tests/term_operation_test.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import requests_mock -import unittest - -from regexsolver import ApiError, RegexSolver, Term - - -class TermsOperationTest(unittest.TestCase): - def setUp(self): - RegexSolver.initialize("TOKEN") - - def test_error_response(self): - with open('tests/assets/response_error.json') as response: - json_response = json.load(response) - with requests_mock.Mocker() as mock: - mock.post( - "https://api.regexsolver.com/v1/compute/intersection", - json=json_response, status_code=400 - ) - - term1 = Term.regex(r"abc") - term2 = Term.regex(r"de") - - try: - term1.intersection(term2) - except ApiError as err: - self.assertEqual( - "The API returned the following error: A random error.", - err.args[0] - ) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..435e10c --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,219 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +import pytest_asyncio + +from regexsolver import ( + ApiError, + Integer, + Length, + RegexSolverClient, + ResponseFormat, +) +from regexsolver.generated import ApiException + +# ========================================== +# FIXTURES +# ========================================== + + +@pytest_asyncio.fixture +def mock_term(): + """Provides a mocked Term object to avoid needing the real implementation.""" + term = MagicMock() + + # Give Pydantic a valid dictionary instead of a MagicMock! + term._api_model = {"type": "regex", "value": "test"} + + # Initialize cache properties to None + term._cardinality = None + term._length = None + term._empty = None + term._empty_string = None + term._total = None + term._pattern = None + term._dot = None + + # Mock the mixin setter so it doesn't throw errors + term._set_properties_mixin = MagicMock() + return term + + +@pytest_asyncio.fixture +async def client(): + """Provides a client with mocked underlying APIs.""" + c = RegexSolverClient(api_token="test-token")._aio + + # Mock out the generated API classes with AsyncMocks + c._analyze_api = AsyncMock() + c._compute_api = AsyncMock() + c._generate_api = AsyncMock() + + yield c + await c.aclose() + + +# ========================================== +# ERROR HANDLING & RATE LIMIT TESTS +# ========================================== + + +@pytest.mark.asyncio +async def test_429_retry_logic(client, mock_term): + """Verifies that the client sleeps and retries on a 429 Too Many Requests.""" + + # Setup the mock to fail once with 429, then succeed + error_429 = ApiException(status=429) + error_429.headers = {"Retry-After": "1"} + + success_response = MagicMock() + success_response.data.value = True + + client._analyze_api.empty.side_effect = [error_429, success_response] + + # Patch asyncio.sleep so we don't actually wait during the test run + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: + result = await client.is_empty(mock_term) + + # Verify the 1-second sleep from the Retry-After header was called + mock_sleep.assert_any_call(1) + + # (Optional) Verify it called sleep twice due to the mock time-freeze side-effect + assert mock_sleep.call_count == 2 + + # Verify it retried and eventually returned True + assert result is True + assert client._analyze_api.empty.call_count == 2 + + +@pytest.mark.asyncio +async def test_api_error_parsing(client, mock_term): + """Verifies that generic ApiExceptions are nicely mapped to your custom ApiError.""" + + error_400 = ApiException(status=400, reason="Bad Request") + error_400.body = '{"success": false, "error": "Invalid regex pattern"}' + + client._analyze_api.length.side_effect = error_400 + + with pytest.raises(ApiError) as exc_info: + await client.get_length(mock_term) + + assert exc_info.value.status_code == 400 + assert "Invalid regex pattern" in str(exc_info.value) + + +# ========================================== +# ANALYZE ENDPOINT TESTS +# ========================================== + + +@pytest.mark.asyncio +async def test_get_cardinality_integer(client, mock_term): + """Tests unwrapping the generated oneOf model into your custom Integer.""" + + # Simulate the messy oneOf generated payload + mock_actual = MagicMock(type="integer", value=42) + mock_response = MagicMock() + mock_response.data.actual_instance = mock_actual + + client._analyze_api.cardinality.return_value = mock_response + + result = await client.get_cardinality(mock_term) + + assert isinstance(result, Integer) + assert result.value == 42 + + # Verify caching: Calling it again shouldn't trigger another network request + await client.get_cardinality(mock_term) + client._analyze_api.cardinality.assert_called_once() + + # Verify the mixin was updated + mock_term._set_properties_mixin.assert_called_once_with(result) + + +@pytest.mark.asyncio +async def test_is_empty_string_side_effects(client, mock_term): + """Verifies that boolean responses properly cache their sibling properties.""" + + mock_response = MagicMock() + mock_response.data.value = True + client._analyze_api.empty_string.return_value = mock_response + + result = await client.is_empty_string(mock_term) + + assert result is True + assert mock_term._empty_string is True + + # If it is only the empty string, it should proactively cache cardinality and length! + assert isinstance(mock_term._cardinality, Integer) + assert mock_term._cardinality.value == 1 + assert isinstance(mock_term._length, Length) + assert mock_term._length.min == 0 + assert mock_term._length.max == 0 + + +# ========================================== +# COMPUTE ENDPOINT TESTS +# ========================================== + + +@pytest.mark.asyncio +async def test_concat(client, mock_term): + """Verifies multi-term requests and Enum format mappings.""" + + # Because we are mocking the request object, we don't need real dictionaries anymore! + term1 = MagicMock(_api_model="dummy_api_model_1") + term2 = MagicMock(_api_model="dummy_api_model_2") + + mock_response = MagicMock() + mock_response.data = "mocked_response_data" + client._compute_api.concat.return_value = mock_response + + # Patch BOTH the custom Term class AND the generated MultiTermsRequest + with ( + patch("regexsolver.client.Term") as MockTermClass, + patch("regexsolver.client.MultiTermsRequest") as MockMultiTermsRequest, + ): + MockTermClass.return_value = "final_term_instance" + + # Tell the mock to return a dummy string instead of a strict Pydantic model + MockMultiTermsRequest.return_value = "perfect_request_payload" + + result = await client.concat(term1, term2, response_format=ResponseFormat.REGEX) + + assert result == "final_term_instance" + + # 1. Verify we passed the exact payload to the generated API + client._compute_api.concat.assert_called_once_with( + multi_terms_request="perfect_request_payload" + ) + + # 2. Verify we constructed the MultiTermsRequest correctly! + MockMultiTermsRequest.assert_called_once() + request_kwargs = MockMultiTermsRequest.call_args.kwargs + + # Check that the raw API models were extracted and passed to the request + assert request_kwargs["terms"] == ["dummy_api_model_1", "dummy_api_model_2"] + + # Check that the options builder successfully attached your Enum! + assert request_kwargs["options"].response.format == ResponseFormat.REGEX + + +# ========================================== +# CONTEXT MANAGER TESTS +# ========================================== + + +@pytest.mark.asyncio +async def test_context_manager(): + """Ensures the client properly closes its session.""" + + with patch("regexsolver.client.ApiClient") as MockApiClient: + mock_instance = MockApiClient.return_value + mock_instance.close = AsyncMock() + + async with RegexSolverClient("token")._aio as c: + assert c is not None + + # Ensure it was safely closed upon exiting the block + mock_instance.close.assert_called_once() From 22b2d4fd1f58e2220c3bf767d50de14b4777bb5b Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:06:54 +0100 Subject: [PATCH 22/47] Update the library structure --- GEMINI.md | 64 +++ README.md | 114 ++--- pyproject.toml | 4 + regexsolver/__init__.py | 22 +- .../{client.py => clients/asynchronous.py} | 420 ++++-------------- regexsolver/clients/rate_limiter.py | 88 ++++ regexsolver/clients/synchronous.py | 344 ++++++++++++++ regexsolver/exceptions.py | 65 ++- regexsolver/models/cardinality.py | 12 + regexsolver/models/term.py | 60 ++- tests/test_async_client.py | 291 ++++++++++++ tests/test_client.py | 219 --------- tests/test_models.py | 138 ++++++ tests/test_rate_limiter.py | 91 ++++ tests/test_sync_client.py | 42 ++ 15 files changed, 1348 insertions(+), 626 deletions(-) create mode 100644 GEMINI.md rename regexsolver/{client.py => clients/asynchronous.py} (58%) create mode 100644 regexsolver/clients/rate_limiter.py create mode 100644 regexsolver/clients/synchronous.py create mode 100644 tests/test_async_client.py delete mode 100644 tests/test_client.py create mode 100644 tests/test_models.py create mode 100644 tests/test_rate_limiter.py create mode 100644 tests/test_sync_client.py diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..2bb38be --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,64 @@ +# RegexSolver Python API Client + +## Project Overview +RegexSolver Python is a client library for the RegexSolver API, providing tools for advanced regular expression operations such as intersection, union, difference, and equivalence analysis. It supports both synchronous and asynchronous usage. + +### Core Technologies +- **Python**: 3.9+ +- **Pydantic**: Data validation and modeling (used for API models). +- **aiohttp**: Asynchronous HTTP client for API communication. +- **OpenAPI Generator**: Used to generate the underlying API client from a shared specification. +- **Pytest**: For unit and integration testing. + +### Architecture +- `regexsolver/`: Main package. + - `clients/`: Contains `RegexSolverClient` (sync) and `AsyncRegexSolverClient` (async) which wrap the generated API calls. + - `models/`: Custom high-level models like `Term` that wrap the generated Pydantic models and provide a clean API for users. + - `generated/`: Code generated by `openapi-generator-cli`. **Do not modify manually.** +- `tests/`: Test suite using `pytest`. + +## Building and Running + +### Installation +To install the project in development mode with all test dependencies: +```bash +pip install -e ".[test]" +``` + +### Running Tests +Tests use `pytest` and `pytest-asyncio`. To run them: +```bash +pytest +``` + +### Linting and Type Checking +The project uses `flake8` for linting and `mypy` for static type checking: +```bash +flake8 regexsolver tests +mypy regexsolver +``` + +### API Generation +The client code in `regexsolver/generated/` is generated from an OpenAPI spec using: +```bash +./generate-api.sh +``` +*Note: This requires `openapi-generator-cli` to be installed and accessible.* + +## Development Conventions + +### Code Style +- Follow **PEP 8** standards. +- Use **Type Hints** for all public methods and properties. +- Publicly exposed classes and functions should be imported into `regexsolver/__init__.py`. + +### Testing Practices +- Use `pytest` for all tests. +- For async code, use `pytest.mark.asyncio`. +- **Mocking**: Use `unittest.mock` (`MagicMock`, `AsyncMock`) to mock API responses and avoid making real network requests during unit tests. +- Ensure that both synchronous and asynchronous clients are tested. + +### API Models +- All user-facing operations involve the `Term` model. +- `Term` objects can be created using `Term.regex(pattern)` or `Term.fair(payload)`. +- Use the generated models from `regexsolver.generated.models` as the underlying data layer for custom models. diff --git a/README.md b/README.md index f9ec186..072f8e9 100644 --- a/README.md +++ b/README.md @@ -6,33 +6,52 @@ ## Installation ```sh -pip install --upgrade regexsolver +pip install regexsolver ``` -Requirements: Python >= 3.7 + +Requirements: **Python >= 3.9** ## Quick Start 1. Create an API token in the [Developer Console](https://console.regexsolver.com/). -2. Initialize the client and start working with terms: +2. Initialize the client and start working with terms. + +### Synchronous Usage + +The synchronous client is the easiest way to get started. ```python -from regexsolver import RegexSolver, Term +from regexsolver import RegexSolverClient, Term -# Set REGEXSOLVER_API_TOKEN in your env and call initialize(), -# or pass the token directly: -RegexSolver.initialize() # or RegexSolver.initialize("YOUR_API_TOKEN") +client = RegexSolverClient("YOUR_API_TOKEN") -# Create terms term1 = Term.regex(r"(abc|de|fg){2,}") term2 = Term.regex(r"de.*") -term3 = Term.regex(r".*abc") -# Compute intersection and difference -result = term1.intersection(term2, term3).difference( - Term.regex(r".+(abc|de).+") -) +is_subset = client.subset(term1, term2) +print(f"Is subset? {is_subset}") +``` + +### Asynchronous Usage + +For high-performance applications, use the asynchronous client. + +```python +import asyncio +from regexsolver import AsyncRegexSolverClient, Term + +client = AsyncRegexSolverClient("YOUR_API_TOKEN") -print(result.get_pattern()) # de(fg)*abc +async def main(): + term1 = Term.regex(r"(abc|de|fg){2,}") + term2 = Term.regex(r"de.*") + + intersection = await client.intersection(term1, term2) + pattern = await client.get_pattern(intersection) + print(pattern) # (abc|de|fg){2,}&de.* + +if __name__ == "__main__": + asyncio.run(main()) ``` ## Key Concepts & Limitations @@ -54,13 +73,13 @@ The API can handle terms in two formats: By default, the engine returns whatever the operation produces, with no extra convertion. Override with `response_format`: ```python -from regexsolver import RegexSolver, ResponseFormat, Term +term1 = Term.regex(r"abcde") +term2 = Term.regex(r"de") -term = Term.regex(r"abcde") -result = term.union(Term.regex(r"de"), response_format=ResponseFormat.REGEX) +result = client.union(term1, term2, response_format=ResponseFormat.REGEX) print(result) # regex=(abc)?de -result = term.union(Term.regex(r"de"), response_format=ResponseFormat.FAIR) +result = client.union(term1, term2, response_format=ResponseFormat.FAIR) print(result) # fair=... ``` @@ -73,15 +92,13 @@ Regardless of the format, you can always call `get_pattern()` to obtain the rege Set a server-side compute timeout in milliseconds with `execution_timeout`: ```python -from regexsolver import ApiError, RegexSolver, Term - -# Limit the server-side compute time to 5 ms +# Limit the server-side compute time to 100 ms try: - res = Term.regex(r".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c").difference( - Term.regex(r".*abc.*"), - execution_timeout=5 - ) -except ApiError as error: + term1 = Term.regex(r".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c") + term2 = Term.regex(r".*abc.*") + + res = client.difference(term1, term2, execution_timeout=100) +except BadRequestError as error: print(error) # The API returned the following error: The operation took too much time. ``` @@ -89,50 +106,37 @@ Timeout is best effort. The exact time is not guaranteed. ## API Overview -`Term` exposes the following methods. - -### Build -| Method | Return | Description | -| -------- | ------- | ------- | -| `Term.fair(fair: str)` | `Term` | Creates a term from a FAIR. | -| `Term.regex(regex: str)` | `Term` | Creates a term from a regex pattern. | +`RegexSolverClient` and `AsyncRegexSolverClient` exposes the following methods. ### Analyze | Method | Return | Description | | -------- | ------- | ------- | -| `t.equivalent(term: Term)` | `bool` | `True` if `t` and `term` accept exactly the same language. Supports `execution_timeout`. | -| `t.get_cardinality()` | `Cardinality` | Returns the cardinality of the term (i.e., the number of possible matched strings). | -| `t.get_dot()` | `str` | Returns a Graphviz DOT representation of the automaton for the term. | -| `t.get_fair()` | `str` | Returns the FAIR of the term if defined. | -| `t.get_length()` | `Length` | Returns the minimum and maximum length of matched strings. | -| `t.get_pattern()` | `str` | Returns a regular expression pattern for the term. | -| `t.is_empty()` | `bool` | `True` if the term matches no string. | -| `t.is_empty_string()` | `bool` | `True` if the term matches only the empty string. | -| `t.is_total()` | `bool` | `True` if the term matches all possible strings. | -| `t.subset(term: Term)` | `bool` | `True` if every string matched by `t` is also matched by `term`. Supports `execution_timeout`. | +| `client.equivalent(t1, t2)` | `bool` | `True` if `t1` and `t2` accept exactly the same language. | +| `client.get_cardinality(t)` | `Cardinality` | Returns the number of possible matched strings. | +| `client.get_dot(t)` | `str` | Returns a Graphviz DOT representation of the automaton. | +| `client.get_length(t)` | `Length` | Returns the minimum and maximum length of matched strings. | +| `client.get_pattern(t)` | `str` | Returns a regular expression pattern for the term. | +| `client.is_empty(t)` | `bool` | `True` if the term matches no string. | +| `client.is_empty_string(t)` | `bool` | `True` if the term matches only the empty string. | +| `client.is_total(t)` | `bool` | `True` if the term matches all possible strings. | +| `client.subset(t1, t2)` | `bool` | `True` if every string matched by `t1` is also matched by `t2`. | ### Compute | Method | Return | Description | | -------- | ------- | ------- | -| `t.concat(*terms: Term)` | `Term` | Concatenates `t` with the given terms. Supports `response_format` and `execution_timeout`. | -| `t.difference(term: Term)` | `Term` | Computes the difference `t - term`. Supports `response_format` and `execution_timeout`. | -| `t.intersection(*terms: Term)` | `Term` | Computes the intersection of `t` with the given terms. Supports `response_format` and `execution_timeout`. | -| `t.repeat(min: int, max: Optional[int])` | `Term` | Computes the repetition of the term between `min` and `max` times; if `max` is `None`, the repetition is unbounded. Supports `response_format` and `execution_timeout`. | -| `t.union(*terms: Term)` | `Term` | Computes the union of `t` with the given terms. Supports `response_format` and `execution_timeout`. | +| `client.concat(*terms)` | `Term` | Concatenates multiple terms in order. | +| `client.difference(t1, t2)` | `Term` | Computes the difference `t1 - t2`. | +| `client.intersection(*terms)` | `Term` | Computes the intersection of the given terms. | +| `client.repeat(t, min, max)` | `Term` | Computes the repetition of the term between `min` and `max` times. | +| `client.union(*terms)` | `Term` | Computes the union of the given terms. | ### Generate | Method | Return | Description | | -------- | ------- | ------- | -| `t.generate_strings(count: int)` | `List[str]` | Generates up to `count` unique example strings matched by `t`. Supports `execution_timeout`. | - -### Other -| Method | Return | Description | -| -------- | ------- | ------- | -| `t.serialize()` | `str` | Returns a serialized form of `t`. | -| `Term.deserialize(string: str)` | `Term` | Returns a deserialized term from the given `string`. | +| `client.generate_strings(t, count)` | `List[str]` | Generates up to `count` unique example strings matched by `t`. | ## Cross-Language Support diff --git a/pyproject.toml b/pyproject.toml index 3de9b9c..1777dba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,3 +63,7 @@ Homepage = "https://regexsolver.com/" Issues = "https://github.com/RegexSolver/regexsolver-python/issues" Documentation = "https://docs.regexsolver.com/" "Source Code" = "https://github.com/RegexSolver/regexsolver-python" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index a3c1d7c..de1ef26 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -1,8 +1,15 @@ -from regexsolver.client import ( - AsyncRegexSolverClient, - RegexSolverClient, +from regexsolver.clients.asynchronous import AsyncRegexSolverClient +from regexsolver.clients.synchronous import RegexSolverClient +from regexsolver.exceptions import ( + ApiError, + BadRequestError, + ForbiddenError, + InternalServerError, + NotFoundError, + RegexSolverError, + TooManyRequestsError, + UnauthorizedError, ) -from regexsolver.exceptions import ApiError from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat @@ -13,6 +20,13 @@ "RegexSolverClient", "Term", "ApiError", + "BadRequestError", + "ForbiddenError", + "InternalServerError", + "NotFoundError", + "RegexSolverError", + "TooManyRequestsError", + "UnauthorizedError", "BigInteger", "Infinite", "Integer", diff --git a/regexsolver/client.py b/regexsolver/clients/asynchronous.py similarity index 58% rename from regexsolver/client.py rename to regexsolver/clients/asynchronous.py index 5bfa293..17487c4 100644 --- a/regexsolver/client.py +++ b/regexsolver/clients/asynchronous.py @@ -1,27 +1,33 @@ import asyncio -import threading -import time +import weakref from typing import List, Optional, Union -from regexsolver.exceptions import ApiError +from regexsolver.clients.rate_limiter import get_rate_limiter +from regexsolver.exceptions import ( + ApiError, + BadRequestError, + ForbiddenError, + InternalServerError, + NotFoundError, + TooManyRequestsError, + UnauthorizedError, +) from regexsolver.generated import ( + AnalyzeApi, + ApiClient, ApiException, + ComputeApi, + Configuration, ErrorResponse, ExecutionOptions, - ResponseOptions, - TwoTermsRequest, -) -from regexsolver.generated.api.analyze_api import AnalyzeApi -from regexsolver.generated.api.compute_api import ComputeApi -from regexsolver.generated.api.generate_api import GenerateApi -from regexsolver.generated.api_client import ApiClient -from regexsolver.generated.configuration import Configuration -from regexsolver.generated.models import ( + GenerateApi, GenerateStringsRequest, MultiTermsRequest, RepeatRequest, RequestOptions, + ResponseOptions, TermRequest, + TwoTermsRequest, ) from regexsolver.models.cardinality import BigInteger, Infinite, Integer from regexsolver.models.length import Length @@ -33,7 +39,7 @@ class AsyncRegexSolverClient: """The Asynchronous Client for RegexSolver. Provides non-blocking access to all RegexSolver API endpoints. - Should be instantiated using an `async with` context manager. + Can be used as a standalone object or as an `async with` context manager. """ def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): @@ -45,12 +51,31 @@ def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): self._compute_api = ComputeApi(self.api_client) self._generate_api = GenerateApi(self.api_client) - self._lock = asyncio.Lock() - self._resume_time = 0.0 + self._rate_limiter = get_rate_limiter(api_token) + + # Ensure the underlying aiohttp session is closed when the client is GC'd. + self._finalizer = weakref.finalize(self, self._run_cleanup, self.api_client) + + @staticmethod + def _run_cleanup(api_client: ApiClient): + """Finalizer callback to safely close the async client. + + Since we cannot await in a finalizer, we try to create a task in the + currently running loop, or just let the session be collected by aiohttp. + """ + try: + loop = asyncio.get_running_loop() + if loop.is_running(): + loop.create_task(api_client.close()) + except RuntimeError: + # No loop is running, we can't do much here. + # aiohttp will eventually emit a warning about unclosed session. + pass async def aclose(self): """Closes the underlying HTTP client session.""" - await self.api_client.close() + if self._finalizer.detach(): + await self.api_client.close() async def __aenter__(self): return self @@ -62,39 +87,23 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): async def _execute_with_retry(self, api_method, **kwargs): max_retries = 5 retries = 0 - while True: - async with self._lock: - sleep_time = self._resume_time - time.time() - - if sleep_time > 0: - await asyncio.sleep(sleep_time) - + await self._rate_limiter.wait() try: return await api_method(**kwargs) - except ApiException as e: if e.status == 429: retries += 1 if retries > max_retries: - raise ApiError( + raise TooManyRequestsError( "Max retries exceeded for 429 Too Many Requests.", status_code=429, ) - - async with self._lock: - sleep_time = self._resume_time - time.time() - if sleep_time <= 0: - headers = e.headers or {} - retry_after = int(headers.get("Retry-After", 1)) - self._resume_time = time.time() + retry_after - sleep_time = retry_after - - await asyncio.sleep(sleep_time) + headers = e.headers or {} + retry_after = float(headers.get("Retry-After", 1)) + await self._rate_limiter.trigger(retry_after) continue - error_msg = e.reason - if e.body: try: parsed_error = ErrorResponse.from_json(e.body) @@ -104,9 +113,31 @@ async def _execute_with_retry(self, api_method, **kwargs): error_msg = e.body except Exception: error_msg = e.body - error_msg = str(error_msg) if error_msg else "Unknown API Error" - raise ApiError(error_msg, status_code=e.status, body=e.body) from None + if e.status == 400: + raise BadRequestError( + error_msg, status_code=e.status, body=e.body + ) from None + elif e.status == 401: + raise UnauthorizedError( + error_msg, status_code=e.status, body=e.body + ) from None + elif e.status == 403: + raise ForbiddenError( + error_msg, status_code=e.status, body=e.body + ) from None + elif e.status == 404: + raise NotFoundError( + error_msg, status_code=e.status, body=e.body + ) from None + elif e.status == 500: + raise InternalServerError( + error_msg, status_code=e.status, body=e.body + ) from None + else: + raise ApiError( + error_msg, status_code=e.status, body=e.body + ) from None def _build_options( self, @@ -114,9 +145,9 @@ def _build_options( response_format: Optional[Union[ResponseFormat, str]] = None, ) -> RequestOptions: options = RequestOptions(schemaVersion=1) - if execution_timeout: + if execution_timeout is not None: options.execution = ExecutionOptions(timeout=execution_timeout) - if response_format: + if response_format is not None: options.response = ResponseOptions(format=response_format) return options @@ -323,8 +354,9 @@ async def get_pattern( Returns: str: A valid regular expression string representing the language. """ - if term._pattern is not None: - return term._pattern + pattern = term.get_pattern() + if pattern is not None: + return pattern request = TermRequest( term=term._api_model, options=self._build_options(execution_timeout) ) @@ -512,305 +544,3 @@ async def generate_strings( self._generate_api.strings, generate_strings_request=request ) return response.data.value - - -class RegexSolverClient: - """Synchronous Client for RegexSolver. - - Exposes all endpoints synchronously by managing a background event loop. - Should be instantiated using a standard `with` context manager. - """ - - def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): - self._aio = AsyncRegexSolverClient(api_token, base_url) - # Run a background event loop so sync methods don't crash in Jupyter/FastAPI - self._loop = asyncio.new_event_loop() - self._thread = threading.Thread(target=self._loop.run_forever, daemon=True) - self._thread.start() - - def _run_sync(self, coro): - """Helper to execute async methods safely from the sync wrapper.""" - future = asyncio.run_coroutine_threadsafe(coro, self._loop) - return future.result() - - def close(self): - """Closes the underlying HTTP client session and stops the background thread.""" - self._run_sync(self._aio.aclose()) - self._loop.call_soon_threadsafe(self._loop.stop) - self._thread.join() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - # --- ANALYZE --- - def get_cardinality(self, term: Term, execution_timeout: Optional[int] = None): - """Computes how many unique strings the term matches. - - Args: - term: The term to analyze. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Cardinality: An object representing either an exact Integer, a BigInteger, or Infinite cardinality. - """ - return self._run_sync(self._aio.get_cardinality(term, execution_timeout)) - - def get_length(self, term: Term, execution_timeout: Optional[int] = None): - """Computes the minimum and maximum length of strings matched by the term. - - Args: - term: The term to analyze. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Length: An object containing `min` and `max` integers. Limits are `None` if unbounded or undefined. - """ - return self._run_sync(self._aio.get_length(term, execution_timeout)) - - def equivalent( - self, term1: Term, term2: Term, execution_timeout: Optional[int] = None - ) -> bool: - """Checks if the two terms accept exactly the same language. - - Args: - term1: The first term. - term2: The second term to compare against. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - bool: True if they are entirely equivalent, False otherwise. - """ - return self._run_sync(self._aio.equivalent(term1, term2, execution_timeout)) - - def subset( - self, - term_subset: Term, - term_superset: Term, - execution_timeout: Optional[int] = None, - ) -> bool: - """Checks if the first term's language is a subset of the second term's language. - - Args: - term_subset: The term to test as the subset. - term_superset: The term representing the entire set space. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - bool: True if every string matched by `term_subset` is also matched by `term_superset`. - """ - return self._run_sync( - self._aio.subset(term_subset, term_superset, execution_timeout) - ) - - def is_empty(self, term: Term, execution_timeout: Optional[int] = None) -> bool: - """Checks if the term matches no strings at all. - - Args: - term: The term to analyze. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - bool: True if the language is completely empty. - """ - return self._run_sync(self._aio.is_empty(term, execution_timeout)) - - def is_empty_string( - self, term: Term, execution_timeout: Optional[int] = None - ) -> bool: - """Checks if the term matches only the empty string. - - Args: - term: The term to analyze. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - bool: True if the term strictly matches the empty string ("") and nothing else. - """ - return self._run_sync(self._aio.is_empty_string(term, execution_timeout)) - - def is_total(self, term: Term, execution_timeout: Optional[int] = None) -> bool: - """Checks if the term matches all possible strings. - - Args: - term: The term to analyze. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - bool: True if the term matches every possible strings. - """ - return self._run_sync(self._aio.is_total(term, execution_timeout)) - - def get_pattern(self, term: Term, execution_timeout: Optional[int] = None) -> str: - """Returns a regular expression pattern that represents the term. - - Args: - term: The term to extract the pattern from. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - str: A valid regular expression string representing the language. - """ - return self._run_sync(self._aio.get_pattern(term, execution_timeout)) - - def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> str: - """Builds a Graphviz DOT representation of the term's automaton. - - Args: - term: The term to visualize. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - str: The raw DOT syntax for Graphviz compilation. - """ - return self._run_sync(self._aio.get_dot(term, execution_timeout)) - - # --- COMPUTE --- - def concat( - self, - *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, - execution_timeout: Optional[int] = None, - ) -> Term: - """Concatenates the given terms sequentially. - - Args: - *terms: A dynamic list of terms to concatenate in order. - response_format: The return format of the term (any, regex or fair). - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Term: A newly computed concatenated term. - """ - return self._run_sync( - self._aio.concat( - *terms, - response_format=response_format, - execution_timeout=execution_timeout, - ) - ) - - def intersection( - self, - *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, - execution_timeout: Optional[int] = None, - ) -> Term: - """Computes the intersection of the given terms. - - Args: - *terms: A dynamic list of terms to intersect. - response_format: The return format of the term (any, regex or fair). - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Term: A term representing only strings matched by ALL provided terms. - """ - return self._run_sync( - self._aio.intersection( - *terms, - response_format=response_format, - execution_timeout=execution_timeout, - ) - ) - - def union( - self, - *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, - execution_timeout: Optional[int] = None, - ) -> Term: - """Computes the union of the given terms. - - Args: - *terms: A dynamic list of terms to combine. - response_format: The return format of the term (any, regex or fair). - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Term: A term representing strings matched by ANY of the provided terms. - """ - return self._run_sync( - self._aio.union( - *terms, - response_format=response_format, - execution_timeout=execution_timeout, - ) - ) - - def difference( - self, - base_term: Term, - excluded_term: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, - execution_timeout: Optional[int] = None, - ) -> Term: - """Computes the difference between the two provided terms. - - Args: - base_term: The base language term to subtract from. - excluded_term: The term whose language should be removed from the base. - response_format: The return format of the term (any, regex or fair). - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Term: A computed difference term. - """ - return self._run_sync( - self._aio.difference( - base_term, - excluded_term, - response_format=response_format, - execution_timeout=execution_timeout, - ) - ) - - def repeat( - self, - term: Term, - min_val: int, - max_val: Optional[int] = None, - response_format: Optional[Union[ResponseFormat, str]] = None, - execution_timeout: Optional[int] = None, - ) -> Term: - """Repeats a term between a minimum and maximum number of times. - - Args: - term: The term to repeat. - min_val: The inclusive lower bound of repetitions. - max_val: The inclusive upper bound. If None, repetitions are unbounded. - response_format: The return format of the term (any, regex or fair). - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - Term: A computed repeated term. - """ - return self._run_sync( - self._aio.repeat( - term, - min_val, - max_val, - response_format=response_format, - execution_timeout=execution_timeout, - ) - ) - - # --- GENERATE --- - def generate_strings( - self, term: Term, count: int, execution_timeout: Optional[int] = None - ) -> List[str]: - """Generates up to `count` unique strings matched by the term. - - Args: - term: The term to sample generated strings from. - count: The maximum number of unique strings to return. - execution_timeout: Timeout in milliseconds for the operation. - - Returns: - List[str]: A list of strings that match the term. - """ - return self._run_sync( - self._aio.generate_strings(term, count, execution_timeout) - ) diff --git a/regexsolver/clients/rate_limiter.py b/regexsolver/clients/rate_limiter.py new file mode 100644 index 0000000..9f43846 --- /dev/null +++ b/regexsolver/clients/rate_limiter.py @@ -0,0 +1,88 @@ +import asyncio +import threading +from typing import Dict, Optional + + +class RateLimiter: + """Shared across all client instances with the same API token and event loop. + + Asyncio primitives (Event, Lock) are NOT thread-safe and are bound to the loop + that created them. This class lazily initializes these primitives to ensure + they are bound to the correct loop. + """ + + def __init__(self): + self._event: Optional[asyncio.Event] = None + self._lock: Optional[asyncio.Lock] = None + self._reopen_task: Optional[asyncio.Task] = None + + def _ensure_primitives(self): + """Lazily initialize asyncio primitives on the current running loop.""" + if self._event is None: + self._event = asyncio.Event() + self._event.set() + if self._lock is None: + self._lock = asyncio.Lock() + + async def wait(self): + """Asynchronously waits until the rate limit is no longer triggered. + + If the limiter is currently triggered (e.g., after a 429 error), this method + will block until the delay has passed. + """ + self._ensure_primitives() + if self._event is None: + raise RuntimeError("RateLimiter event not initialized.") + await self._event.wait() + + async def trigger(self, retry_after: float): + """Triggers the rate limiter for a specific duration. + + Args: + retry_after: The duration in seconds to keep the limiter triggered. + + This method will cause all subsequent calls to wait() to block until + the duration has elapsed. + """ + self._ensure_primitives() + if self._lock is None or self._event is None: + raise RuntimeError("RateLimiter primitives not initialized.") + async with self._lock: + if not self._event.is_set(): + return # already being handled + self._event.clear() + if self._reopen_task and not self._reopen_task.done(): + self._reopen_task.cancel() + self._reopen_task = asyncio.create_task(self._lift(retry_after)) + + async def _lift(self, delay: float): + """Background task that lifts the rate limit after the specified delay. + + Args: + delay: The delay in seconds. + """ + await asyncio.sleep(delay) + if self._event is None: + raise RuntimeError("RateLimiter event not initialized.") + self._event.set() + + +_rate_limiters: Dict[tuple, RateLimiter] = {} +_registry_lock = threading.Lock() + + +def get_rate_limiter(api_token: str) -> RateLimiter: + """Returns a RateLimiter instance for the given token and current event loop.""" + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # If no loop is running, we can't reliably provide a loop-bound limiter. + # This shouldn't happen during normal client usage as methods are called + # within a loop. + loop = None + + key = (api_token, loop) + with _registry_lock: + if key not in _rate_limiters: + _rate_limiters[key] = RateLimiter() + return _rate_limiters[key] diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py new file mode 100644 index 0000000..cd1e64a --- /dev/null +++ b/regexsolver/clients/synchronous.py @@ -0,0 +1,344 @@ +import asyncio +import threading +import weakref +from typing import List, Optional, Union + +from regexsolver.clients.asynchronous import AsyncRegexSolverClient +from regexsolver.models.response_format import ResponseFormat +from regexsolver.models.term import Term + +# Global state for the shared background event loop +_SHARED_LOOP: Optional[asyncio.AbstractEventLoop] = None +_SHARED_THREAD: Optional[threading.Thread] = None +_SHARED_LOCK = threading.Lock() + + +def _get_or_create_shared_loop() -> asyncio.AbstractEventLoop: + """Retrieves the shared global event loop, creating and starting it if necessary.""" + global _SHARED_LOOP, _SHARED_THREAD + with _SHARED_LOCK: + if _SHARED_LOOP is None or _SHARED_THREAD is None or _SHARED_THREAD.is_alive(): + _SHARED_LOOP = asyncio.new_event_loop() + _SHARED_THREAD = threading.Thread( + target=_SHARED_LOOP.run_forever, + name="RegexSolverSyncWorker", + daemon=True, + ) + _SHARED_THREAD.start() + return _SHARED_LOOP + + +class RegexSolverClient: + """Synchronous Client for RegexSolver. + + Exposes all endpoints synchronously by managing a shared background event loop. + While it supports manual `.close()`, it is best used as a context manager. + """ + + def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + self._loop = _get_or_create_shared_loop() + self._aio = AsyncRegexSolverClient(api_token, base_url) + + # Ensure the async client is closed even if the user forgets to call close() or use 'with' + self._finalizer = weakref.finalize( + self, self._run_cleanup, self._aio, self._loop + ) + + @staticmethod + def _run_cleanup( + aio_client: AsyncRegexSolverClient, loop: asyncio.AbstractEventLoop + ): + """Finalizer callback to safely close the async client in the background loop.""" + if loop.is_running(): + asyncio.run_coroutine_threadsafe(aio_client.aclose(), loop) + + def _run_sync(self, coro): + """Helper to execute async methods safely from the sync wrapper.""" + # Use a longer timeout or allow it to be infinite since the server + # already has its own execution_timeout logic. + future = asyncio.run_coroutine_threadsafe(coro, self._loop) + return future.result(timeout=None) + + def close(self): + """Closes the underlying HTTP client session. + + The shared background thread remains running for other client instances. + """ + if self._finalizer.detach(): + self._run_sync(self._aio.aclose()) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # --- ANALYZE --- + def get_cardinality(self, term: Term, execution_timeout: Optional[int] = None): + """Computes how many unique strings the term matches. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Cardinality: An object representing either an exact Integer, a BigInteger, or Infinite cardinality. + """ + return self._run_sync(self._aio.get_cardinality(term, execution_timeout)) + + def get_length(self, term: Term, execution_timeout: Optional[int] = None): + """Computes the minimum and maximum length of strings matched by the term. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Length: An object containing `min` and `max` integers. Limits are `None` if unbounded or undefined. + """ + return self._run_sync(self._aio.get_length(term, execution_timeout)) + + def equivalent( + self, term1: Term, term2: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the two terms accept exactly the same language. + + Args: + term1: The first term. + term2: The second term to compare against. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if they are entirely equivalent, False otherwise. + """ + return self._run_sync(self._aio.equivalent(term1, term2, execution_timeout)) + + def subset( + self, + term_subset: Term, + term_superset: Term, + execution_timeout: Optional[int] = None, + ) -> bool: + """Checks if the first term's language is a subset of the second term's language. + + Args: + term_subset: The term to test as the subset. + term_superset: The term representing the entire set space. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if every string matched by `term_subset` is also matched by `term_superset`. + """ + return self._run_sync( + self._aio.subset(term_subset, term_superset, execution_timeout) + ) + + def is_empty(self, term: Term, execution_timeout: Optional[int] = None) -> bool: + """Checks if the term matches no strings at all. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the language is completely empty. + """ + return self._run_sync(self._aio.is_empty(term, execution_timeout)) + + def is_empty_string( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Checks if the term matches only the empty string. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term strictly matches the empty string ("") and nothing else. + """ + return self._run_sync(self._aio.is_empty_string(term, execution_timeout)) + + def is_total(self, term: Term, execution_timeout: Optional[int] = None) -> bool: + """Checks if the term matches all possible strings. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term matches every possible strings. + """ + return self._run_sync(self._aio.is_total(term, execution_timeout)) + + def get_pattern(self, term: Term, execution_timeout: Optional[int] = None) -> str: + """Returns a regular expression pattern that represents the term. + + Args: + term: The term to extract the pattern from. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: A valid regular expression string representing the language. + """ + return self._run_sync(self._aio.get_pattern(term, execution_timeout)) + + def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> str: + """Builds a Graphviz DOT representation of the term's automaton. + + Args: + term: The term to visualize. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + str: The raw DOT syntax for Graphviz compilation. + """ + return self._run_sync(self._aio.get_dot(term, execution_timeout)) + + # --- COMPUTE --- + def concat( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Concatenates the given terms sequentially. + + Args: + *terms: A dynamic list of terms to concatenate in order. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A newly computed concatenated term. + """ + return self._run_sync( + self._aio.concat( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def intersection( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the intersection of the given terms. + + Args: + *terms: A dynamic list of terms to intersect. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing only strings matched by ALL provided terms. + """ + return self._run_sync( + self._aio.intersection( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def union( + self, + *terms: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the union of the given terms. + + Args: + *terms: A dynamic list of terms to combine. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A term representing strings matched by ANY of the provided terms. + """ + return self._run_sync( + self._aio.union( + *terms, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def difference( + self, + base_term: Term, + excluded_term: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the difference between the two provided terms. + + Args: + base_term: The base language term to subtract from. + excluded_term: The term whose language should be removed from the base. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed difference term. + """ + return self._run_sync( + self._aio.difference( + base_term, + excluded_term, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + def repeat( + self, + term: Term, + min_val: int, + max_val: Optional[int] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Repeats a term between a minimum and maximum number of times. + + Args: + term: The term to repeat. + min_val: The inclusive lower bound of repetitions. + max_val: The inclusive upper bound. If None, repetitions are unbounded. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A computed repeated term. + """ + return self._run_sync( + self._aio.repeat( + term, + min_val, + max_val, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + + # --- GENERATE --- + def generate_strings( + self, term: Term, count: int, execution_timeout: Optional[int] = None + ) -> List[str]: + """Generates up to `count` unique strings matched by the term. + + Args: + term: The term to sample generated strings from. + count: The maximum number of unique strings to return. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + List[str]: A list of strings that match the term. + """ + return self._run_sync( + self._aio.generate_strings(term, count, execution_timeout) + ) diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 30ad429..7517aff 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -8,7 +8,12 @@ class RegexSolverError(Exception): class ApiError(RegexSolverError): - """Raised when the RegexSolver API returns an error response.""" + """Base exception raised when the RegexSolver API returns an error response. + + Attributes: + status_code (Optional[int]): The HTTP status code returned by the API. + body (Optional[str]): The raw string body of the error response. + """ def __init__( self, @@ -19,3 +24,61 @@ def __init__( super().__init__(message) self.status_code = status_code self.body = body + + +class BadRequestError(ApiError): + """Raised when the API returns a 400 Bad Request error. + + Usually indicates one of the following issues: + - The provided regular expression is invalid or cannot be parsed. + - The requested `execution_timeout` exceeds the maximum allowed for your current plan. + - The number of terms provided in a multi-term operation exceeds the maximum allowed. + - The execution of the request exceeds the provided `execution_timeout` or the maximum allowed for your current plan. + """ + + pass + + +class UnauthorizedError(ApiError): + """Raised when the API returns a 401 Unauthorized error. + + Indicates that the provided authentication token is missing, malformed, or invalid. + """ + + pass + + +class ForbiddenError(ApiError): + """Raised when the API returns a 403 Forbidden error. + + Usually indicates that your account's monthly compute quota has been exceeded. + """ + + pass + + +class NotFoundError(ApiError): + """Raised when the API returns a 404 Not Found error. + + Indicates that the requested API endpoint or resource does not exist. + """ + + pass + + +class TooManyRequestsError(ApiError): + """Raised when the API returns a 429 Too Many Requests error and max retries are exceeded. + + Indicates that your requests-per-second (req/s) rate limit has been exceeded. + """ + + pass + + +class InternalServerError(ApiError): + """Raised when the API returns a 500 Internal Server Error. + + Indicates an unexpected failure or panic on the RegexSolver compute servers. + """ + + pass diff --git a/regexsolver/models/cardinality.py b/regexsolver/models/cardinality.py index cee6c7e..be553a4 100644 --- a/regexsolver/models/cardinality.py +++ b/regexsolver/models/cardinality.py @@ -9,6 +9,9 @@ class Cardinality(TermPropertiesMixin): pass + def __repr__(self) -> str: + return "" + @dataclass(frozen=True) class Infinite(Cardinality): @@ -20,6 +23,9 @@ def is_empty(self) -> Optional[bool]: def is_empty_string(self) -> Optional[bool]: return False + def __repr__(self) -> str: + return "" + @dataclass(frozen=True) class BigInteger(Cardinality): @@ -34,6 +40,9 @@ def is_empty_string(self) -> Optional[bool]: def is_total(self) -> Optional[bool]: return False + def __repr__(self) -> str: + return "" + @dataclass(frozen=True) class Integer(Cardinality): @@ -55,3 +64,6 @@ def is_empty_string(self) -> Optional[bool]: def is_total(self) -> Optional[bool]: return False + + def __repr__(self) -> str: + return f"" diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index 7d9500a..d592d78 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -1,3 +1,5 @@ +import re +from re import Pattern from typing import Optional, Union, cast from regexsolver.generated.models import Term as GeneratedTerm @@ -30,6 +32,7 @@ def __init__(self, generated_term: GeneratedTerm): self._total: Optional[bool] = None self._pattern: Optional[str] = None self._dot: Optional[str] = None + self._compiled_regex: Optional[Pattern] = None @property def _actual_model(self) -> Union[TermRegex, TermFair]: @@ -76,7 +79,7 @@ def fair(cls, fair: str) -> "Term": Returns: Term: A new Term instance representing the FAIR payload. """ - gen_term = GeneratedTerm(TermFair(type="regex", value=fair)) + gen_term = GeneratedTerm(TermFair(type="fair", value=fair)) return cls(gen_term) @classmethod @@ -96,12 +99,62 @@ def get_fair(self) -> Optional[str]: """Retrieves the FAIR payload if the term was explicitly constructed as one. Returns: - Optional[str]: The FAIR payload string, or None if the term is a standard regex. + Optional[str]: The FAIR payload string, or None if the term is a regex. """ if self.type == "fair": return self.value return None + def get_pattern(self) -> Optional[str]: + """Retrieves the term pattern if the term was explicitly constructed as a regex, or if the pattern was previously computed with the client. + + Returns: + Optional[str]: The term pattern as a string, or None if the term is a FAIR and the pattern was not previously computed. + """ + if self.type == "regex": + return self.value + elif self._pattern is not None: + return self._pattern + return None + + def is_match(self, string: str) -> Optional[bool]: + """Evaluates if a string matches the term using Python's native `re` module. + + Note: The RegexSolver engine is designed for pattern analysis and + computation, not string evaluation. Therefore, this string matching + feature is executed entirely client-side. + + This method strictly enforces RegexSolver's language rules + by anchoring the expression (requiring a full string match) and allowing + the dot ('.') to match line feeds. The compiled regular expression is + cached on the instance for high-performance repeated matching. + + Args: + string (str): The string to test against the term's pattern. + + Returns: + Optional[bool]: True if the string exactly matches, False if it does not, + or None if the term's pattern is currently unknown (e.g., it is a FAIR + term whose pattern hasn't been computed by the client yet). + + Raises: + ValueError: If the term's pattern contains syntax supported by RegexSolver + but unsupported by Python's native `re` engine. + """ + pattern = self.get_pattern() + if pattern is None: + return None + + if self._compiled_regex is None: + try: + self._compiled_regex = re.compile(pattern, flags=re.DOTALL) + except re.error as e: + raise ValueError( + f"Pattern '{pattern}' cannot be evaluated by Python's re module: {e}" + ) + + return self._compiled_regex.fullmatch(string) is not None + def serialize(self) -> str: """Serializes the Term into a portable string format. @@ -142,3 +195,6 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: """Generates a hash based on the serialized string representation.""" return hash(self.serialize()) + + def __repr__(self) -> str: + return f"" diff --git a/tests/test_async_client.py b/tests/test_async_client.py new file mode 100644 index 0000000..f0fa81d --- /dev/null +++ b/tests/test_async_client.py @@ -0,0 +1,291 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from regexsolver import ( + ApiError, + AsyncRegexSolverClient, + BadRequestError, + Infinite, + Integer, + Term, +) +from regexsolver.generated import ApiException + + +@pytest.fixture +async def async_client(): + client = AsyncRegexSolverClient(api_token="test-token") + client._analyze_api = AsyncMock() + client._compute_api = AsyncMock() + client._generate_api = AsyncMock() + yield client + await client.aclose() + + +@pytest.mark.asyncio +async def test_get_cardinality_integer(async_client): + term = Term.regex("abc") + + mock_response = MagicMock() + mock_response.data.actual_instance.type = "integer" + mock_response.data.actual_instance.value = 42 + async_client._analyze_api.cardinality.return_value = mock_response + + result = await async_client.get_cardinality(term) + assert isinstance(result, Integer) + assert result.value == 42 + assert term._cardinality == result + + +@pytest.mark.asyncio +async def test_get_cardinality_infinite(async_client): + term = Term.regex(".*") + + mock_response = MagicMock() + mock_response.data.actual_instance.type = "infinite" + async_client._analyze_api.cardinality.return_value = mock_response + + result = await async_client.get_cardinality(term) + assert isinstance(result, Infinite) + + +@pytest.mark.asyncio +async def test_get_length(async_client): + term = Term.regex("abc") + + mock_response = MagicMock() + mock_response.data.min = 3 + mock_response.data.max = 3 + async_client._analyze_api.length.return_value = mock_response + + result = await async_client.get_length(term) + assert result.min == 3 + assert result.max == 3 + + +@pytest.mark.asyncio +async def test_is_empty(async_client): + term = Term.regex("[]") + + mock_response = MagicMock() + mock_response.data.value = True + async_client._analyze_api.empty.return_value = mock_response + + result = await async_client.is_empty(term) + assert result is True + assert term._empty is True + + +@pytest.mark.asyncio +async def test_compute_union(async_client): + term1 = Term.regex("a") + term2 = Term.regex("b") + + mock_response = MagicMock() + # mock_response.data should be a GeneratedTerm + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "a|b" + async_client._compute_api.union.return_value = mock_response + + result = await async_client.union(term1, term2) + assert isinstance(result, Term) + assert result.value == "a|b" + + +@pytest.mark.asyncio +async def test_error_handling_400(async_client): + term = Term.regex("invalid[") + + error_400 = ApiException(status=400, reason="Bad Request") + error_400.body = '{"error": "Invalid regex"}' + async_client._analyze_api.empty.side_effect = error_400 + + with pytest.raises(BadRequestError) as exc_info: + await async_client.is_empty(term) + assert "Invalid regex" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_error_handling_401(async_client): + async_client._analyze_api.empty.side_effect = ApiException( + status=401, reason="Unauthorized" + ) + from regexsolver import UnauthorizedError + + with pytest.raises(UnauthorizedError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_403(async_client): + async_client._analyze_api.empty.side_effect = ApiException( + status=403, reason="Forbidden" + ) + from regexsolver import ForbiddenError + + with pytest.raises(ForbiddenError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_404(async_client): + async_client._analyze_api.empty.side_effect = ApiException( + status=404, reason="Not Found" + ) + from regexsolver import NotFoundError + + with pytest.raises(NotFoundError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_500(async_client): + async_client._analyze_api.empty.side_effect = ApiException( + status=500, reason="Internal Server Error" + ) + from regexsolver import InternalServerError + + with pytest.raises(InternalServerError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_other(async_client): + async_client._analyze_api.empty.side_effect = ApiException( + status=418, reason="I'm a teapot" + ) + with pytest.raises(ApiError) as exc_info: + await async_client.is_empty(Term.regex("abc")) + assert exc_info.value.status_code == 418 + + +@pytest.mark.asyncio +async def test_retry_on_429(async_client): + term = Term.regex("abc") + + error_429 = ApiException(status=429) + error_429.headers = {"Retry-After": "0.1"} + + success_response = MagicMock() + success_response.data.value = True + + async_client._analyze_api.empty.side_effect = [error_429, success_response] + + with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: + result = await async_client.is_empty(term) + assert result is True + mock_sleep.assert_called() + + +@pytest.mark.asyncio +async def test_equivalent(async_client): + term1 = Term.regex("a") + term2 = Term.regex("a") + mock_response = MagicMock() + mock_response.data.value = True + async_client._analyze_api.equivalent.return_value = mock_response + assert await async_client.equivalent(term1, term2) is True + + +@pytest.mark.asyncio +async def test_subset(async_client): + term1 = Term.regex("a") + term2 = Term.regex("a|b") + mock_response = MagicMock() + mock_response.data.value = True + async_client._analyze_api.subset.return_value = mock_response + assert await async_client.subset(term1, term2) is True + + +@pytest.mark.asyncio +async def test_is_empty_string(async_client): + term = Term.regex("") + mock_response = MagicMock() + mock_response.data.value = True + async_client._analyze_api.empty_string.return_value = mock_response + assert await async_client.is_empty_string(term) is True + + +@pytest.mark.asyncio +async def test_is_total(async_client): + term = Term.regex(".*") + mock_response = MagicMock() + mock_response.data.value = True + async_client._analyze_api.total.return_value = mock_response + assert await async_client.is_total(term) is True + + +@pytest.mark.asyncio +async def test_get_pattern(async_client): + term = Term.regex("a") + mock_response = MagicMock() + mock_response.data.value = "a" + async_client._analyze_api.pattern.return_value = mock_response + assert await async_client.get_pattern(term) == "a" + + +@pytest.mark.asyncio +async def test_get_dot(async_client): + term = Term.regex("a") + mock_response = MagicMock() + mock_response.data.value = "digraph {...}" + async_client._analyze_api.dot.return_value = mock_response + assert await async_client.get_dot(term) == "digraph {...}" + + +@pytest.mark.asyncio +async def test_concat(async_client): + term1 = Term.regex("a") + term2 = Term.regex("b") + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "ab" + async_client._compute_api.concat.return_value = mock_response + result = await async_client.concat(term1, term2) + assert result.value == "ab" + + +@pytest.mark.asyncio +async def test_intersection(async_client): + term1 = Term.regex("a.") + term2 = Term.regex(".b") + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "ab" + async_client._compute_api.intersection.return_value = mock_response + result = await async_client.intersection(term1, term2) + assert result.value == "ab" + + +@pytest.mark.asyncio +async def test_difference(async_client): + term1 = Term.regex("a|b") + term2 = Term.regex("b") + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "a" + async_client._compute_api.difference.return_value = mock_response + result = await async_client.difference(term1, term2) + assert result.value == "a" + + +@pytest.mark.asyncio +async def test_repeat(async_client): + term = Term.regex("a") + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "a{2,3}" + async_client._compute_api.repeat.return_value = mock_response + result = await async_client.repeat(term, 2, 3) + assert result.value == "a{2,3}" + + +@pytest.mark.asyncio +async def test_generate_strings(async_client): + term = Term.regex("a*") + mock_response = MagicMock() + mock_response.data.value = ["", "a", "aa"] + async_client._generate_api.strings.return_value = mock_response + result = await async_client.generate_strings(term, 3) + assert result == ["", "a", "aa"] diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 435e10c..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,219 +0,0 @@ -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -import pytest_asyncio - -from regexsolver import ( - ApiError, - Integer, - Length, - RegexSolverClient, - ResponseFormat, -) -from regexsolver.generated import ApiException - -# ========================================== -# FIXTURES -# ========================================== - - -@pytest_asyncio.fixture -def mock_term(): - """Provides a mocked Term object to avoid needing the real implementation.""" - term = MagicMock() - - # Give Pydantic a valid dictionary instead of a MagicMock! - term._api_model = {"type": "regex", "value": "test"} - - # Initialize cache properties to None - term._cardinality = None - term._length = None - term._empty = None - term._empty_string = None - term._total = None - term._pattern = None - term._dot = None - - # Mock the mixin setter so it doesn't throw errors - term._set_properties_mixin = MagicMock() - return term - - -@pytest_asyncio.fixture -async def client(): - """Provides a client with mocked underlying APIs.""" - c = RegexSolverClient(api_token="test-token")._aio - - # Mock out the generated API classes with AsyncMocks - c._analyze_api = AsyncMock() - c._compute_api = AsyncMock() - c._generate_api = AsyncMock() - - yield c - await c.aclose() - - -# ========================================== -# ERROR HANDLING & RATE LIMIT TESTS -# ========================================== - - -@pytest.mark.asyncio -async def test_429_retry_logic(client, mock_term): - """Verifies that the client sleeps and retries on a 429 Too Many Requests.""" - - # Setup the mock to fail once with 429, then succeed - error_429 = ApiException(status=429) - error_429.headers = {"Retry-After": "1"} - - success_response = MagicMock() - success_response.data.value = True - - client._analyze_api.empty.side_effect = [error_429, success_response] - - # Patch asyncio.sleep so we don't actually wait during the test run - with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: - result = await client.is_empty(mock_term) - - # Verify the 1-second sleep from the Retry-After header was called - mock_sleep.assert_any_call(1) - - # (Optional) Verify it called sleep twice due to the mock time-freeze side-effect - assert mock_sleep.call_count == 2 - - # Verify it retried and eventually returned True - assert result is True - assert client._analyze_api.empty.call_count == 2 - - -@pytest.mark.asyncio -async def test_api_error_parsing(client, mock_term): - """Verifies that generic ApiExceptions are nicely mapped to your custom ApiError.""" - - error_400 = ApiException(status=400, reason="Bad Request") - error_400.body = '{"success": false, "error": "Invalid regex pattern"}' - - client._analyze_api.length.side_effect = error_400 - - with pytest.raises(ApiError) as exc_info: - await client.get_length(mock_term) - - assert exc_info.value.status_code == 400 - assert "Invalid regex pattern" in str(exc_info.value) - - -# ========================================== -# ANALYZE ENDPOINT TESTS -# ========================================== - - -@pytest.mark.asyncio -async def test_get_cardinality_integer(client, mock_term): - """Tests unwrapping the generated oneOf model into your custom Integer.""" - - # Simulate the messy oneOf generated payload - mock_actual = MagicMock(type="integer", value=42) - mock_response = MagicMock() - mock_response.data.actual_instance = mock_actual - - client._analyze_api.cardinality.return_value = mock_response - - result = await client.get_cardinality(mock_term) - - assert isinstance(result, Integer) - assert result.value == 42 - - # Verify caching: Calling it again shouldn't trigger another network request - await client.get_cardinality(mock_term) - client._analyze_api.cardinality.assert_called_once() - - # Verify the mixin was updated - mock_term._set_properties_mixin.assert_called_once_with(result) - - -@pytest.mark.asyncio -async def test_is_empty_string_side_effects(client, mock_term): - """Verifies that boolean responses properly cache their sibling properties.""" - - mock_response = MagicMock() - mock_response.data.value = True - client._analyze_api.empty_string.return_value = mock_response - - result = await client.is_empty_string(mock_term) - - assert result is True - assert mock_term._empty_string is True - - # If it is only the empty string, it should proactively cache cardinality and length! - assert isinstance(mock_term._cardinality, Integer) - assert mock_term._cardinality.value == 1 - assert isinstance(mock_term._length, Length) - assert mock_term._length.min == 0 - assert mock_term._length.max == 0 - - -# ========================================== -# COMPUTE ENDPOINT TESTS -# ========================================== - - -@pytest.mark.asyncio -async def test_concat(client, mock_term): - """Verifies multi-term requests and Enum format mappings.""" - - # Because we are mocking the request object, we don't need real dictionaries anymore! - term1 = MagicMock(_api_model="dummy_api_model_1") - term2 = MagicMock(_api_model="dummy_api_model_2") - - mock_response = MagicMock() - mock_response.data = "mocked_response_data" - client._compute_api.concat.return_value = mock_response - - # Patch BOTH the custom Term class AND the generated MultiTermsRequest - with ( - patch("regexsolver.client.Term") as MockTermClass, - patch("regexsolver.client.MultiTermsRequest") as MockMultiTermsRequest, - ): - MockTermClass.return_value = "final_term_instance" - - # Tell the mock to return a dummy string instead of a strict Pydantic model - MockMultiTermsRequest.return_value = "perfect_request_payload" - - result = await client.concat(term1, term2, response_format=ResponseFormat.REGEX) - - assert result == "final_term_instance" - - # 1. Verify we passed the exact payload to the generated API - client._compute_api.concat.assert_called_once_with( - multi_terms_request="perfect_request_payload" - ) - - # 2. Verify we constructed the MultiTermsRequest correctly! - MockMultiTermsRequest.assert_called_once() - request_kwargs = MockMultiTermsRequest.call_args.kwargs - - # Check that the raw API models were extracted and passed to the request - assert request_kwargs["terms"] == ["dummy_api_model_1", "dummy_api_model_2"] - - # Check that the options builder successfully attached your Enum! - assert request_kwargs["options"].response.format == ResponseFormat.REGEX - - -# ========================================== -# CONTEXT MANAGER TESTS -# ========================================== - - -@pytest.mark.asyncio -async def test_context_manager(): - """Ensures the client properly closes its session.""" - - with patch("regexsolver.client.ApiClient") as MockApiClient: - mock_instance = MockApiClient.return_value - mock_instance.close = AsyncMock() - - async with RegexSolverClient("token")._aio as c: - assert c is not None - - # Ensure it was safely closed upon exiting the block - mock_instance.close.assert_called_once() diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..f163fe4 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,138 @@ +from regexsolver.generated.models import Term as GeneratedTerm +from regexsolver.models.cardinality import BigInteger, Infinite, Integer +from regexsolver.models.length import Length +from regexsolver.models.term import Term + + +def test_term_creation_regex(): + term = Term.regex("abc") + assert term.type == "regex" + assert term.value == "abc" + assert isinstance(term._api_model, GeneratedTerm) + + +def test_term_creation_fair(): + term = Term.fair("fair_payload") + assert term.type == "fair" + assert term.value == "fair_payload" + + +def test_cardinality_integer(): + c = Integer(10) + assert c.value == 10 + assert c.is_empty() is False + assert c.is_empty_string() is False + assert c.is_total() is False + assert repr(c) == "" + + +def test_cardinality_integer_zero(): + c = Integer(0) + assert c.is_empty() is True + assert c.is_empty_string() is False + + +def test_cardinality_integer_one(): + c = Integer(1) + assert c.is_empty() is False + assert c.is_empty_string() is None # Per implementation + + +def test_cardinality_big_integer(): + c = BigInteger() + assert c.is_empty() is False + assert c.is_empty_string() is False + assert c.is_total() is False + assert repr(c) == "" + + +def test_cardinality_infinite(): + c = Infinite() + assert c.is_empty() is False + assert c.is_empty_string() is False + assert repr(c) == "" + + +def test_length(): + lenght = Length(min=1, max=5) + assert lenght.min == 1 + assert lenght.max == 5 + assert lenght.is_empty() is False + assert lenght.is_empty_string() is False + assert lenght.is_total() is False + assert repr(lenght) == "" + + +def test_length_empty(): + lenght = Length(min=None, max=None) + assert lenght.is_empty() is True + + +def test_length_empty_string(): + lenght = Length(min=0, max=0) + assert lenght.is_empty_string() is True + + +def test_length_total_candidate(): + lenght = Length(min=0, max=None) + assert lenght.is_total() is None # Implementation returns None if it COULD be total + + +def test_term_properties_caching(): + term = Term.regex("abc") + assert term._cardinality is None + + c = Integer(5) + term._cardinality = c + # Simulate AsyncRegexSolverClient behavior + term._set_properties_mixin(c) + + assert term._cardinality == c + # Since Integer(5).is_empty() is False, it should set _empty to False + assert term._empty is False + + +def test_term_get_fair_and_pattern(): + regex_term = Term.regex("abc") + assert regex_term.get_pattern() == "abc" + assert regex_term.get_fair() is None + + fair_term = Term.fair("payload") + assert fair_term.get_fair() == "payload" + assert fair_term.get_pattern() is None + + fair_term._pattern = "abc" + assert fair_term.get_pattern() == "abc" + + +def test_term_serialize_deserialize(): + term = Term.regex("abc") + serialized = term.serialize() + assert serialized == "regex=abc" + assert str(term) == "regex=abc" + + deserialized = Term.deserialize(serialized) + assert deserialized == term + assert hash(deserialized) == hash(term) + + fair_term = Term.fair("payload") + assert Term.deserialize(fair_term.serialize()) == fair_term + + assert Term.deserialize("invalid") is None + assert Term.deserialize("unknown=value") is None + + +def test_term_is_match(): + term = Term.regex("a.b") + assert term.is_match("axb") is True + assert term.is_match("a\nb") is True # DOTALL + assert term.is_match("ab") is False + assert term.is_match("axxb") is False # anchored (fullmatch) + + fair_term = Term.fair("payload") + assert fair_term.is_match("abc") is None + + +def test_term_repr(): + term = Term.regex("abc") + assert repr(term) == "" diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py new file mode 100644 index 0000000..8b550c9 --- /dev/null +++ b/tests/test_rate_limiter.py @@ -0,0 +1,91 @@ +import asyncio + +import pytest + +from regexsolver.clients.rate_limiter import RateLimiter, get_rate_limiter + + +@pytest.mark.asyncio +async def test_rate_limiter_wait(): + rl = RateLimiter() + # Primitives are None before use + assert rl._event is None + # Initially set after wait ensures it + await rl.wait() + assert rl._event is not None + assert rl._event.is_set() + + +@pytest.mark.asyncio +async def test_rate_limiter_trigger(): + rl = RateLimiter() + await rl.trigger(0.1) + # trigger ensures primitives + assert rl._event is not None + assert not rl._event.is_set() + + await asyncio.sleep(0.15) + assert rl._event.is_set() + + +@pytest.mark.asyncio +async def test_rate_limiter_trigger_already_cleared(): + rl = RateLimiter() + await rl.trigger(0.2) + task1 = rl._reopen_task + + # Trigger again while still clearing + await rl.trigger(0.1) + # It should not have changed the event or task if handled correctly + # (actually the implementation returns if not set) + assert rl._event is not None + assert not rl._event.is_set() + assert rl._reopen_task == task1 + + +def test_get_rate_limiter_loop_aware(): + # We can't easily start multiple loops in one sync test easily without some boilerplate, + # but we can verify the singleton logic still works for the same loop. + rl1 = get_rate_limiter("token1") + rl2 = get_rate_limiter("token1") + assert rl1 is rl2 + + +@pytest.mark.asyncio +async def test_get_rate_limiter_different_loops(): + # In an async test, get_running_loop() works. + rl1 = get_rate_limiter("token1") + + async def other_loop_task(): + new_loop = asyncio.new_event_loop() + try: + # We must run this in the context of the new loop + # But get_rate_limiter uses get_running_loop() + # So we use the new loop to run a call. + def call_in_loop(): + return get_rate_limiter("token1") + + rl2 = new_loop.run_until_complete( + asyncio.to_thread(call_in_loop) + ) # This is getting complicated + # Simpler: just mock the loop or use a separate thread + return rl2 + finally: + new_loop.close() + + # Let's just use a thread to get a different loop context + import threading + + rl2_container = [] + + def thread_target(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + rl2_container.append(get_rate_limiter("token1")) + loop.close() + + t = threading.Thread(target=thread_target) + t.start() + t.join() + + assert rl1 is not rl2_container[0] diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py new file mode 100644 index 0000000..5bc6c86 --- /dev/null +++ b/tests/test_sync_client.py @@ -0,0 +1,42 @@ +from unittest.mock import AsyncMock + +from regexsolver import Integer, RegexSolverClient, Term + + +def test_sync_client_get_cardinality(): + with RegexSolverClient(api_token="test-token") as client: + # Mock the underlying async client's method + client._aio.get_cardinality = AsyncMock(return_value=Integer(42)) + + term = Term.regex("abc") + result = client.get_cardinality(term) + + assert isinstance(result, Integer) + assert result.value == 42 + client._aio.get_cardinality.assert_called_once_with(term, None) + + +def test_sync_client_is_empty(): + with RegexSolverClient(api_token="test-token") as client: + client._aio.is_empty = AsyncMock(return_value=False) + + term = Term.regex("abc") + result = client.is_empty(term) + + assert result is False + client._aio.is_empty.assert_called_once_with(term, None) + + +def test_sync_client_union(): + with RegexSolverClient(api_token="test-token") as client: + mock_result_term = Term.regex("a|b") + client._aio.union = AsyncMock(return_value=mock_result_term) + + term1 = Term.regex("a") + term2 = Term.regex("b") + result = client.union(term1, term2) + + assert result == mock_result_term + client._aio.union.assert_called_once_with( + term1, term2, response_format=None, execution_timeout=None + ) From bea97db607042706f8d8a1f8e8aebb0ea21b9793 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:09:53 +0100 Subject: [PATCH 23/47] Update error handling --- README.md | 3 +- regexsolver/__init__.py | 16 ++++ regexsolver/clients/asynchronous.py | 51 ++++++---- regexsolver/exceptions.py | 61 +++++++++--- .../generated/models/error_response.py | 8 +- tests/test_async_client.py | 94 ++++++++++++++++++- 6 files changed, 194 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 072f8e9..830e302 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,7 @@ async def main(): pattern = await client.get_pattern(intersection) print(pattern) # (abc|de|fg){2,}&de.* -if __name__ == "__main__": - asyncio.run(main()) +asyncio.run(main()) ``` ## Key Concepts & Limitations diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index de1ef26..9fabe8f 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -5,9 +5,17 @@ BadRequestError, ForbiddenError, InternalServerError, + InvalidJsonError, + InvalidTokenError, + MissingOrMalformedTokenError, NotFoundError, + QuotaExceededError, RegexSolverError, + TimeoutExceededError, + TimeoutTooLargeError, TooManyRequestsError, + TooManyStringsToGenerateError, + TooManyTermsError, UnauthorizedError, ) from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer @@ -23,9 +31,17 @@ "BadRequestError", "ForbiddenError", "InternalServerError", + "InvalidJsonError", + "InvalidTokenError", + "MissingOrMalformedTokenError", "NotFoundError", + "QuotaExceededError", "RegexSolverError", + "TimeoutExceededError", + "TimeoutTooLargeError", "TooManyRequestsError", + "TooManyStringsToGenerateError", + "TooManyTermsError", "UnauthorizedError", "BigInteger", "Infinite", diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 17487c4..2dd4b84 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -8,8 +8,16 @@ BadRequestError, ForbiddenError, InternalServerError, + InvalidJsonError, + InvalidTokenError, + MissingOrMalformedTokenError, NotFoundError, + QuotaExceededError, + TimeoutExceededError, + TimeoutTooLargeError, TooManyRequestsError, + TooManyStringsToGenerateError, + TooManyTermsError, UnauthorizedError, ) from regexsolver.generated import ( @@ -104,40 +112,47 @@ async def _execute_with_retry(self, api_method, **kwargs): await self._rate_limiter.trigger(retry_after) continue error_msg = e.reason + error_code = None if e.body: try: parsed_error = ErrorResponse.from_json(e.body) if parsed_error is not None: error_msg = parsed_error.error + error_code = parsed_error.error_code else: error_msg = e.body except Exception: error_msg = e.body error_msg = str(error_msg) if error_msg else "Unknown API Error" + if e.status == 400: - raise BadRequestError( - error_msg, status_code=e.status, body=e.body - ) from None + if error_code == "InvalidJson": + raise InvalidJsonError(error_msg, status_code=e.status, body=e.body) from None + elif error_code == "TooManyTerms": + raise TooManyTermsError(error_msg, status_code=e.status, body=e.body) from None + elif error_code == "TimeoutTooLarge": + raise TimeoutTooLargeError(error_msg, status_code=e.status, body=e.body) from None + elif error_code == "TimeoutExceeded": + raise TimeoutExceededError(error_msg, status_code=e.status, body=e.body) from None + elif error_code == "TooManyStringsToGenerate": + raise TooManyStringsToGenerateError(error_msg, status_code=e.status, body=e.body) from None + raise BadRequestError(error_msg, status_code=e.status, body=e.body) from None elif e.status == 401: - raise UnauthorizedError( - error_msg, status_code=e.status, body=e.body - ) from None + if error_code == "MissingOrMalformedToken": + raise MissingOrMalformedTokenError(error_msg, status_code=e.status, body=e.body) from None + elif error_code == "InvalidToken": + raise InvalidTokenError(error_msg, status_code=e.status, body=e.body) from None + raise UnauthorizedError(error_msg, status_code=e.status, body=e.body) from None elif e.status == 403: - raise ForbiddenError( - error_msg, status_code=e.status, body=e.body - ) from None + if error_code == "QuotaExceeded": + raise QuotaExceededError(error_msg, status_code=e.status, body=e.body) from None + raise ForbiddenError(error_msg, status_code=e.status, body=e.body) from None elif e.status == 404: - raise NotFoundError( - error_msg, status_code=e.status, body=e.body - ) from None + raise NotFoundError(error_msg, status_code=e.status, body=e.body) from None elif e.status == 500: - raise InternalServerError( - error_msg, status_code=e.status, body=e.body - ) from None + raise InternalServerError(error_msg, status_code=e.status, body=e.body) from None else: - raise ApiError( - error_msg, status_code=e.status, body=e.body - ) from None + raise ApiError(error_msg, status_code=e.status, body=e.body) from None def _build_options( self, diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 7517aff..e801032 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -27,32 +27,67 @@ def __init__( class BadRequestError(ApiError): - """Raised when the API returns a 400 Bad Request error. + """Raised when the API returns a 400 Bad Request error.""" - Usually indicates one of the following issues: - - The provided regular expression is invalid or cannot be parsed. - - The requested `execution_timeout` exceeds the maximum allowed for your current plan. - - The number of terms provided in a multi-term operation exceeds the maximum allowed. - - The execution of the request exceeds the provided `execution_timeout` or the maximum allowed for your current plan. - """ + pass + + +class InvalidJsonError(BadRequestError): + """Raised when the provided JSON is invalid or cannot be parsed.""" + + pass + + +class TooManyTermsError(BadRequestError): + """Raised when the number of terms provided exceeds the maximum allowed.""" + + pass + + +class TimeoutTooLargeError(BadRequestError): + """Raised when the requested `execution_timeout` exceeds the maximum allowed for your current plan.""" + + pass + + +class TimeoutExceededError(BadRequestError): + """Raised when the execution of the request exceeds the provided `execution_timeout` or the maximum allowed for your current plan.""" + + pass + + +class TooManyStringsToGenerateError(BadRequestError): + """Raised when the requested number of strings to generate exceeds the maximum allowed.""" pass class UnauthorizedError(ApiError): - """Raised when the API returns a 401 Unauthorized error. + """Raised when the API returns a 401 Unauthorized error.""" - Indicates that the provided authentication token is missing, malformed, or invalid. - """ + pass + + +class MissingOrMalformedTokenError(UnauthorizedError): + """Raised when the provided authentication token is missing or malformed.""" + + pass + + +class InvalidTokenError(UnauthorizedError): + """Raised when the provided authentication token is invalid.""" pass class ForbiddenError(ApiError): - """Raised when the API returns a 403 Forbidden error. + """Raised when the API returns a 403 Forbidden error.""" - Usually indicates that your account's monthly compute quota has been exceeded. - """ + pass + + +class QuotaExceededError(ForbiddenError): + """Raised when your account's monthly compute quota has been exceeded.""" pass diff --git a/regexsolver/generated/models/error_response.py b/regexsolver/generated/models/error_response.py index bd9a5d1..1ff5dc1 100644 --- a/regexsolver/generated/models/error_response.py +++ b/regexsolver/generated/models/error_response.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self @@ -28,7 +28,8 @@ class ErrorResponse(BaseModel): """ # noqa: E501 success: StrictBool error: StrictStr = Field(description="Human readable error message.") - __properties: ClassVar[List[str]] = ["success", "error"] + error_code: Optional[StrictStr] = Field(default=None, description="The error code.", alias="errorCode") + __properties: ClassVar[List[str]] = ["success", "error", "errorCode"] model_config = ConfigDict( populate_by_name=True, @@ -82,7 +83,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "success": obj.get("success"), - "error": obj.get("error") + "error": obj.get("error"), + "errorCode": obj.get("errorCode") }) return _obj diff --git a/tests/test_async_client.py b/tests/test_async_client.py index f0fa81d..60b87ca 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -6,9 +6,20 @@ ApiError, AsyncRegexSolverClient, BadRequestError, + ForbiddenError, Infinite, Integer, + InvalidJsonError, + InvalidTokenError, + MissingOrMalformedTokenError, + NotFoundError, + QuotaExceededError, Term, + TimeoutExceededError, + TimeoutTooLargeError, + TooManyStringsToGenerateError, + TooManyTermsError, + UnauthorizedError, ) from regexsolver.generated import ApiException @@ -106,12 +117,91 @@ async def test_error_handling_400(async_client): assert "Invalid regex" in str(exc_info.value) +@pytest.mark.asyncio +async def test_error_handling_invalid_json(async_client): + error_400 = ApiException(status=400) + error_400.body = ( + '{"success": false, "error": "Invalid JSON", "errorCode": "InvalidJson"}' + ) + async_client._analyze_api.empty.side_effect = error_400 + with pytest.raises(InvalidJsonError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_too_many_terms(async_client): + error_400 = ApiException(status=400) + error_400.body = ( + '{"success": false, "error": "Too many terms", "errorCode": "TooManyTerms"}' + ) + async_client._compute_api.union.side_effect = error_400 + with pytest.raises(TooManyTermsError): + await async_client.union(Term.regex("a"), Term.regex("b")) + + +@pytest.mark.asyncio +async def test_error_handling_timeout_too_large(async_client): + error_400 = ApiException(status=400) + error_400.body = '{"success": false, "error": "Timeout too large", "errorCode": "TimeoutTooLarge"}' + async_client._analyze_api.empty.side_effect = error_400 + with pytest.raises(TimeoutTooLargeError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_timeout_exceeded(async_client): + error_400 = ApiException(status=400) + error_400.body = '{"success": false, "error": "Timeout exceeded", "errorCode": "TimeoutExceeded"}' + async_client._analyze_api.empty.side_effect = error_400 + with pytest.raises(TimeoutExceededError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_too_many_strings_to_generate(async_client): + error_400 = ApiException(status=400) + error_400.body = '{"success": false, "error": "Too many strings", "errorCode": "TooManyStringsToGenerate"}' + async_client._generate_api.strings.side_effect = error_400 + with pytest.raises(TooManyStringsToGenerateError): + await async_client.generate_strings(Term.regex("abc"), 1000) + + +@pytest.mark.asyncio +async def test_error_handling_missing_or_malformed_token(async_client): + error_401 = ApiException(status=401) + error_401.body = '{"success": false, "error": "Missing token", "errorCode": "MissingOrMalformedToken"}' + async_client._analyze_api.empty.side_effect = error_401 + with pytest.raises(MissingOrMalformedTokenError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_invalid_token(async_client): + error_401 = ApiException(status=401) + error_401.body = ( + '{"success": false, "error": "Invalid token", "errorCode": "InvalidToken"}' + ) + async_client._analyze_api.empty.side_effect = error_401 + with pytest.raises(InvalidTokenError): + await async_client.is_empty(Term.regex("abc")) + + +@pytest.mark.asyncio +async def test_error_handling_quota_exceeded(async_client): + error_403 = ApiException(status=403) + error_403.body = ( + '{"success": false, "error": "Quota exceeded", "errorCode": "QuotaExceeded"}' + ) + async_client._analyze_api.empty.side_effect = error_403 + with pytest.raises(QuotaExceededError): + await async_client.is_empty(Term.regex("abc")) + + @pytest.mark.asyncio async def test_error_handling_401(async_client): async_client._analyze_api.empty.side_effect = ApiException( status=401, reason="Unauthorized" ) - from regexsolver import UnauthorizedError with pytest.raises(UnauthorizedError): await async_client.is_empty(Term.regex("abc")) @@ -122,7 +212,6 @@ async def test_error_handling_403(async_client): async_client._analyze_api.empty.side_effect = ApiException( status=403, reason="Forbidden" ) - from regexsolver import ForbiddenError with pytest.raises(ForbiddenError): await async_client.is_empty(Term.regex("abc")) @@ -133,7 +222,6 @@ async def test_error_handling_404(async_client): async_client._analyze_api.empty.side_effect = ApiException( status=404, reason="Not Found" ) - from regexsolver import NotFoundError with pytest.raises(NotFoundError): await async_client.is_empty(Term.regex("abc")) From 6e55a6691bf6bd214ccadb1cf10b30fe73eea466 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:37:04 +0100 Subject: [PATCH 24/47] Update workflow --- .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++++++++++++++++ GEMINI.md | 40 +++++++++++++++++++--------- README.md | 2 +- 3 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ea0eeda --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test]" + + - name: Lint with flake8 + run: flake8 regexsolver tests + + - name: Type check with mypy + run: mypy regexsolver + + test: + name: Test (Python ${{ matrix.python-version }}) + needs: lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test]" + + - name: Run tests with pytest + run: pytest diff --git a/GEMINI.md b/GEMINI.md index 2bb38be..d103957 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -12,8 +12,15 @@ RegexSolver Python is a client library for the RegexSolver API, providing tools ### Architecture - `regexsolver/`: Main package. - - `clients/`: Contains `RegexSolverClient` (sync) and `AsyncRegexSolverClient` (async) which wrap the generated API calls. - - `models/`: Custom high-level models like `Term` that wrap the generated Pydantic models and provide a clean API for users. + - `clients/`: Contains `RegexSolverClient` (sync) and `AsyncRegexSolverClient` (async). + - `RegexSolverClient`: Wrapper that manages a background event loop to provide a synchronous API. + - `AsyncRegexSolverClient`: Core implementation using `aiohttp`. + - `models/`: Custom high-level models. + - `Term`: Primary object for Regex or FAIR patterns. Caches computed properties. + - `Cardinality`: Represents `Integer`, `BigInteger`, or `Infinite`. + - `Length`: Represents `min` and `max` matched string lengths. + - `ResponseFormat`: Enum for `any`, `regex`, or `fair`. + - `exceptions.py`: Detailed hierarchy mapping API errors to Python exceptions. - `generated/`: Code generated by `openapi-generator-cli`. **Do not modify manually.** - `tests/`: Test suite using `pytest`. @@ -47,18 +54,27 @@ The client code in `regexsolver/generated/` is generated from an OpenAPI spec us ## Development Conventions -### Code Style -- Follow **PEP 8** standards. -- Use **Type Hints** for all public methods and properties. -- Publicly exposed classes and functions should be imported into `regexsolver/__init__.py`. +### Core Domain Logic +- **Term Caching**: `Term` instances cache properties like `cardinality`, `length`, `empty`, `total`, `pattern`, and `dot` once computed by the client to minimize redundant API calls. +- **Client-side Matching**: `Term.is_match(string)` is performed client-side using Python's `re` module. It uses `re.DOTALL` and anchors the match to the full string. +- **Serialization**: `Term.serialize()` produces a `type=value` string (e.g., `regex=[a-z]`), which can be restored via `Term.deserialize(string)`. + +### Request Options +All client methods support: +- `execution_timeout`: Optional integer (milliseconds) to limit engine execution time. +- `response_format`: For `compute` operations, specifies the desired format of the returned `Term`. + +### Exception Handling +The library maps HTTP status codes and API `error_code` values to specific exceptions: +- `400 Bad Request`: `BadRequestError` (Subclasses: `InvalidJsonError`, `TooManyTermsError`, `TimeoutTooLargeError`, `TimeoutExceededError`, `TooManyStringsToGenerateError`). +- `401 Unauthorized`: `UnauthorizedError` (Subclasses: `MissingOrMalformedTokenError`, `InvalidTokenError`). +- `403 Forbidden`: `ForbiddenError` (Subclasses: `QuotaExceededError`). +- `404 Not Found`: `NotFoundError`. +- `429 Too Many Requests`: `TooManyRequestsError` (raised after 5 retries). +- `500 Internal Server Error`: `InternalServerError`. ### Testing Practices - Use `pytest` for all tests. - For async code, use `pytest.mark.asyncio`. -- **Mocking**: Use `unittest.mock` (`MagicMock`, `AsyncMock`) to mock API responses and avoid making real network requests during unit tests. +- **Mocking**: Use `unittest.mock` (`MagicMock`, `AsyncMock`) to mock API responses. - Ensure that both synchronous and asynchronous clients are tested. - -### API Models -- All user-facing operations involve the `Term` model. -- `Term` objects can be created using `Term.regex(pattern)` or `Term.fair(payload)`. -- Use the generated models from `regexsolver.generated.models` as the underlying data layer for custom models. diff --git a/README.md b/README.md index 830e302..0410993 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ try: term2 = Term.regex(r".*abc.*") res = client.difference(term1, term2, execution_timeout=100) -except BadRequestError as error: +except TimeoutExceeded as error: print(error) # The API returned the following error: The operation took too much time. ``` From 06b381496338708d6eca9ff0bed3761379761a95 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:21:34 +0100 Subject: [PATCH 25/47] Update README.md --- README.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 0410993..2566dec 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ client = RegexSolverClient("YOUR_API_TOKEN") term1 = Term.regex(r"(abc|de|fg){2,}") term2 = Term.regex(r"de.*") -is_subset = client.subset(term1, term2) -print(f"Is subset? {is_subset}") +intersection = client.intersection(term1, term2) +pattern = client.get_pattern(intersection) +print(pattern) # de(abc|de|fg)+ ``` ### Asynchronous Usage @@ -37,18 +38,15 @@ print(f"Is subset? {is_subset}") For high-performance applications, use the asynchronous client. ```python -import asyncio -from regexsolver import AsyncRegexSolverClient, Term - -client = AsyncRegexSolverClient("YOUR_API_TOKEN") - async def main(): - term1 = Term.regex(r"(abc|de|fg){2,}") - term2 = Term.regex(r"de.*") + async with AsyncRegexSolverClient("YOUR_API_TOKEN") as client: + term1 = Term.regex(r"(abc|de|fg){2,}") + term2 = Term.regex(r"de.*") + + intersection = await client.intersection(term1, term2) + pattern = await client.get_pattern(intersection) + print(pattern) # de(abc|de|fg)+ - intersection = await client.intersection(term1, term2) - pattern = await client.get_pattern(intersection) - print(pattern) # (abc|de|fg){2,}&de.* asyncio.run(main()) ``` From c59c88eede482443189df8ace17c958c56605210 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:56:23 +0100 Subject: [PATCH 26/47] Add logging --- regexsolver/clients/asynchronous.py | 70 +++++++++++++++++++++++------ regexsolver/clients/rate_limiter.py | 8 ++++ regexsolver/clients/synchronous.py | 13 +++++- 3 files changed, 76 insertions(+), 15 deletions(-) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 2dd4b84..9f6cc2b 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -1,4 +1,5 @@ import asyncio +import logging import weakref from typing import List, Optional, Union @@ -42,6 +43,8 @@ from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import Term +logger = logging.getLogger(__name__) + class AsyncRegexSolverClient: """The Asynchronous Client for RegexSolver. @@ -51,6 +54,7 @@ class AsyncRegexSolverClient: """ def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + logger.debug("Initializing AsyncRegexSolverClient.") self.configuration = Configuration(host=base_url, access_token=api_token) self.api_client = ApiClient(self.configuration) self.api_client.user_agent = "RegexSolver Python / 1.1.0" @@ -83,6 +87,7 @@ def _run_cleanup(api_client: ApiClient): async def aclose(self): """Closes the underlying HTTP client session.""" if self._finalizer.detach(): + logger.debug("Closing AsyncRegexSolverClient.") await self.api_client.close() async def __aenter__(self): @@ -103,12 +108,17 @@ async def _execute_with_retry(self, api_method, **kwargs): if e.status == 429: retries += 1 if retries > max_retries: + logger.error("Max retries exceeded for 429 Too Many Requests.") raise TooManyRequestsError( "Max retries exceeded for 429 Too Many Requests.", status_code=429, ) headers = e.headers or {} retry_after = float(headers.get("Retry-After", 1)) + logger.debug( + f"429 Too Many Requests hit (Attempt {retries}/{max_retries}). " + f"Triggering rate limiter for {retry_after} seconds." + ) await self._rate_limiter.trigger(retry_after) continue error_msg = e.reason @@ -124,35 +134,67 @@ async def _execute_with_retry(self, api_method, **kwargs): except Exception: error_msg = e.body error_msg = str(error_msg) if error_msg else "Unknown API Error" + error_code = str(error_code) if error_code else "UnknownError" + logger.error( + f"RegexSolver API request failed with status {e.status}: {error_code}/{error_msg}" + ) if e.status == 400: if error_code == "InvalidJson": - raise InvalidJsonError(error_msg, status_code=e.status, body=e.body) from None + raise InvalidJsonError( + error_msg, status_code=e.status, body=e.body + ) from None elif error_code == "TooManyTerms": - raise TooManyTermsError(error_msg, status_code=e.status, body=e.body) from None + raise TooManyTermsError( + error_msg, status_code=e.status, body=e.body + ) from None elif error_code == "TimeoutTooLarge": - raise TimeoutTooLargeError(error_msg, status_code=e.status, body=e.body) from None + raise TimeoutTooLargeError( + error_msg, status_code=e.status, body=e.body + ) from None elif error_code == "TimeoutExceeded": - raise TimeoutExceededError(error_msg, status_code=e.status, body=e.body) from None + raise TimeoutExceededError( + error_msg, status_code=e.status, body=e.body + ) from None elif error_code == "TooManyStringsToGenerate": - raise TooManyStringsToGenerateError(error_msg, status_code=e.status, body=e.body) from None - raise BadRequestError(error_msg, status_code=e.status, body=e.body) from None + raise TooManyStringsToGenerateError( + error_msg, status_code=e.status, body=e.body + ) from None + raise BadRequestError( + error_msg, status_code=e.status, body=e.body + ) from None elif e.status == 401: if error_code == "MissingOrMalformedToken": - raise MissingOrMalformedTokenError(error_msg, status_code=e.status, body=e.body) from None + raise MissingOrMalformedTokenError( + error_msg, status_code=e.status, body=e.body + ) from None elif error_code == "InvalidToken": - raise InvalidTokenError(error_msg, status_code=e.status, body=e.body) from None - raise UnauthorizedError(error_msg, status_code=e.status, body=e.body) from None + raise InvalidTokenError( + error_msg, status_code=e.status, body=e.body + ) from None + raise UnauthorizedError( + error_msg, status_code=e.status, body=e.body + ) from None elif e.status == 403: if error_code == "QuotaExceeded": - raise QuotaExceededError(error_msg, status_code=e.status, body=e.body) from None - raise ForbiddenError(error_msg, status_code=e.status, body=e.body) from None + raise QuotaExceededError( + error_msg, status_code=e.status, body=e.body + ) from None + raise ForbiddenError( + error_msg, status_code=e.status, body=e.body + ) from None elif e.status == 404: - raise NotFoundError(error_msg, status_code=e.status, body=e.body) from None + raise NotFoundError( + error_msg, status_code=e.status, body=e.body + ) from None elif e.status == 500: - raise InternalServerError(error_msg, status_code=e.status, body=e.body) from None + raise InternalServerError( + error_msg, status_code=e.status, body=e.body + ) from None else: - raise ApiError(error_msg, status_code=e.status, body=e.body) from None + raise ApiError( + error_msg, status_code=e.status, body=e.body + ) from None def _build_options( self, diff --git a/regexsolver/clients/rate_limiter.py b/regexsolver/clients/rate_limiter.py index 9f43846..3fb4c2a 100644 --- a/regexsolver/clients/rate_limiter.py +++ b/regexsolver/clients/rate_limiter.py @@ -1,7 +1,10 @@ import asyncio +import logging import threading from typing import Dict, Optional +logger = logging.getLogger(__name__) + class RateLimiter: """Shared across all client instances with the same API token and event loop. @@ -50,6 +53,9 @@ async def trigger(self, retry_after: float): async with self._lock: if not self._event.is_set(): return # already being handled + logger.debug( + f"Rate limit triggered. Delaying operations for {retry_after} seconds." + ) self._event.clear() if self._reopen_task and not self._reopen_task.done(): self._reopen_task.cancel() @@ -64,6 +70,7 @@ async def _lift(self, delay: float): await asyncio.sleep(delay) if self._event is None: raise RuntimeError("RateLimiter event not initialized.") + logger.debug("Rate limit lifted. Resuming operations.") self._event.set() @@ -84,5 +91,6 @@ def get_rate_limiter(api_token: str) -> RateLimiter: key = (api_token, loop) with _registry_lock: if key not in _rate_limiters: + logger.debug("Creating new RateLimiter instance for current event loop.") _rate_limiters[key] = RateLimiter() return _rate_limiters[key] diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index cd1e64a..2324af7 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -1,4 +1,5 @@ import asyncio +import logging import threading import weakref from typing import List, Optional, Union @@ -7,6 +8,8 @@ from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import Term +logger = logging.getLogger(__name__) + # Global state for the shared background event loop _SHARED_LOOP: Optional[asyncio.AbstractEventLoop] = None _SHARED_THREAD: Optional[threading.Thread] = None @@ -17,7 +20,12 @@ def _get_or_create_shared_loop() -> asyncio.AbstractEventLoop: """Retrieves the shared global event loop, creating and starting it if necessary.""" global _SHARED_LOOP, _SHARED_THREAD with _SHARED_LOCK: - if _SHARED_LOOP is None or _SHARED_THREAD is None or _SHARED_THREAD.is_alive(): + if ( + _SHARED_LOOP is None + or _SHARED_THREAD is None + or not _SHARED_THREAD.is_alive() + ): + logger.debug("Starting shared RegexSolver background event loop thread.") _SHARED_LOOP = asyncio.new_event_loop() _SHARED_THREAD = threading.Thread( target=_SHARED_LOOP.run_forever, @@ -36,6 +44,7 @@ class RegexSolverClient: """ def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + logger.debug("Initializing RegexSolverClient.") self._loop = _get_or_create_shared_loop() self._aio = AsyncRegexSolverClient(api_token, base_url) @@ -50,6 +59,7 @@ def _run_cleanup( ): """Finalizer callback to safely close the async client in the background loop.""" if loop.is_running(): + logger.debug("Closing RegexSolverClient.") asyncio.run_coroutine_threadsafe(aio_client.aclose(), loop) def _run_sync(self, coro): @@ -65,6 +75,7 @@ def close(self): The shared background thread remains running for other client instances. """ if self._finalizer.detach(): + logger.debug("Closing RegexSolverClient.") self._run_sync(self._aio.aclose()) def __enter__(self): From b5ef629f658e6c2a01ee6a3a60e939652bc88398 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:37:41 +0100 Subject: [PATCH 27/47] Update generate strings --- regexsolver/clients/asynchronous.py | 19 ++++++++++++------- regexsolver/clients/synchronous.py | 11 ++++++++--- .../models/generate_strings_request.py | 8 +++++--- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 9f6cc2b..d5536ae 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -98,25 +98,24 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): # --- HELPER --- async def _execute_with_retry(self, api_method, **kwargs): - max_retries = 5 - retries = 0 + retried = False while True: await self._rate_limiter.wait() try: return await api_method(**kwargs) except ApiException as e: if e.status == 429: - retries += 1 - if retries > max_retries: + if retried: logger.error("Max retries exceeded for 429 Too Many Requests.") raise TooManyRequestsError( "Max retries exceeded for 429 Too Many Requests.", status_code=429, ) + retried = True headers = e.headers or {} retry_after = float(headers.get("Retry-After", 1)) logger.debug( - f"429 Too Many Requests hit (Attempt {retries}/{max_retries}). " + "429 Too Many Requests hit. " f"Triggering rate limiter for {retry_after} seconds." ) await self._rate_limiter.trigger(retry_after) @@ -580,13 +579,18 @@ async def repeat( # --- GENERATE --- async def generate_strings( - self, term: Term, count: int, execution_timeout: Optional[int] = None + self, + term: Term, + count: int, + offset: int, + execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `count` unique strings matched by the term. + """Generates up to `count` distinct strings matched by 'term', skipping the first 'offset' strings. Args: term: The term to sample generated strings from. count: The maximum number of unique strings to return. + offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -595,6 +599,7 @@ async def generate_strings( request = GenerateStringsRequest( term=term._api_model, count=count, + offset=offset, options=self._build_options(execution_timeout), ) response = await self._execute_with_retry( diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index 2324af7..84ef3b9 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -338,18 +338,23 @@ def repeat( # --- GENERATE --- def generate_strings( - self, term: Term, count: int, execution_timeout: Optional[int] = None + self, + term: Term, + count: int, + offset: int, + execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `count` unique strings matched by the term. + """Generates up to `count` distinct strings matched by 'term', skipping the first 'offset' strings. Args: term: The term to sample generated strings from. count: The maximum number of unique strings to return. + offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. Returns: List[str]: A list of strings that match the term. """ return self._run_sync( - self._aio.generate_strings(term, count, execution_timeout) + self._aio.generate_strings(term, count, offset, execution_timeout) ) diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/generated/models/generate_strings_request.py index ae8862d..837f6bb 100644 --- a/regexsolver/generated/models/generate_strings_request.py +++ b/regexsolver/generated/models/generate_strings_request.py @@ -26,12 +26,13 @@ class GenerateStringsRequest(BaseModel): """ - Request to generate up to 'count' distinct strings matched by 'term'. + Request to generate up to 'count' distinct strings matched by 'term', skipping the first 'offset' strings. """ # noqa: E501 - term: Term = Field(description="Source term to sample from.") + term: Term = Field(description="Source term to generate strings from.") count: StrictInt = Field(description="Maximum number of unique strings to return.") + offset: StrictInt = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") options: Optional[RequestOptions] = None - __properties: ClassVar[List[str]] = ["term", "count", "options"] + __properties: ClassVar[List[str]] = ["term", "count", "offset", "options"] model_config = ConfigDict( populate_by_name=True, @@ -92,6 +93,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, "count": obj.get("count"), + "offset": obj.get("offset"), "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None }) return _obj From a894e419fc554e46f3b4282007e3a7bc75cdd43c Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:16:37 +0100 Subject: [PATCH 28/47] Update generate_strings --- regexsolver/__init__.py | 4 ++-- regexsolver/clients/asynchronous.py | 6 +++--- regexsolver/exceptions.py | 4 ++-- .../generated/models/generate_strings_request.py | 3 ++- tests/test_async_client.py | 10 +++++----- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 9fabe8f..1a51ad5 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -6,6 +6,7 @@ ForbiddenError, InternalServerError, InvalidJsonError, + InvalidNumberOfStringsToGenerate, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, @@ -14,7 +15,6 @@ TimeoutExceededError, TimeoutTooLargeError, TooManyRequestsError, - TooManyStringsToGenerateError, TooManyTermsError, UnauthorizedError, ) @@ -40,7 +40,7 @@ "TimeoutExceededError", "TimeoutTooLargeError", "TooManyRequestsError", - "TooManyStringsToGenerateError", + "InvalidNumberOfStringsToGenerate", "TooManyTermsError", "UnauthorizedError", "BigInteger", diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index d5536ae..2e7ed81 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -10,6 +10,7 @@ ForbiddenError, InternalServerError, InvalidJsonError, + InvalidNumberOfStringsToGenerate, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, @@ -17,7 +18,6 @@ TimeoutExceededError, TimeoutTooLargeError, TooManyRequestsError, - TooManyStringsToGenerateError, TooManyTermsError, UnauthorizedError, ) @@ -155,8 +155,8 @@ async def _execute_with_retry(self, api_method, **kwargs): raise TimeoutExceededError( error_msg, status_code=e.status, body=e.body ) from None - elif error_code == "TooManyStringsToGenerate": - raise TooManyStringsToGenerateError( + elif error_code == "InvalidNumberOfStringsToGenerate": + raise InvalidNumberOfStringsToGenerate( error_msg, status_code=e.status, body=e.body ) from None raise BadRequestError( diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index e801032..8e46f6c 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -56,8 +56,8 @@ class TimeoutExceededError(BadRequestError): pass -class TooManyStringsToGenerateError(BadRequestError): - """Raised when the requested number of strings to generate exceeds the maximum allowed.""" +class InvalidNumberOfStringsToGenerate(BadRequestError): + """Raised when the requested number of strings to generate is below the minimum or exceeds the maximum allowed.""" pass diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/generated/models/generate_strings_request.py index 837f6bb..15cc5e1 100644 --- a/regexsolver/generated/models/generate_strings_request.py +++ b/regexsolver/generated/models/generate_strings_request.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictInt from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated from regexsolver.generated.models.request_options import RequestOptions from regexsolver.generated.models.term import Term from typing import Optional, Set @@ -29,7 +30,7 @@ class GenerateStringsRequest(BaseModel): Request to generate up to 'count' distinct strings matched by 'term', skipping the first 'offset' strings. """ # noqa: E501 term: Term = Field(description="Source term to generate strings from.") - count: StrictInt = Field(description="Maximum number of unique strings to return.") + count: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") offset: StrictInt = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") options: Optional[RequestOptions] = None __properties: ClassVar[List[str]] = ["term", "count", "offset", "options"] diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 60b87ca..126c5a4 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -10,6 +10,7 @@ Infinite, Integer, InvalidJsonError, + InvalidNumberOfStringsToGenerate, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, @@ -17,7 +18,6 @@ Term, TimeoutExceededError, TimeoutTooLargeError, - TooManyStringsToGenerateError, TooManyTermsError, UnauthorizedError, ) @@ -160,10 +160,10 @@ async def test_error_handling_timeout_exceeded(async_client): @pytest.mark.asyncio async def test_error_handling_too_many_strings_to_generate(async_client): error_400 = ApiException(status=400) - error_400.body = '{"success": false, "error": "Too many strings", "errorCode": "TooManyStringsToGenerate"}' + error_400.body = '{"success": false, "error": "Too many strings", "errorCode": "InvalidNumberOfStringsToGenerate"}' async_client._generate_api.strings.side_effect = error_400 - with pytest.raises(TooManyStringsToGenerateError): - await async_client.generate_strings(Term.regex("abc"), 1000) + with pytest.raises(InvalidNumberOfStringsToGenerate): + await async_client.generate_strings(Term.regex("abc"), 100, 0) @pytest.mark.asyncio @@ -375,5 +375,5 @@ async def test_generate_strings(async_client): mock_response = MagicMock() mock_response.data.value = ["", "a", "aa"] async_client._generate_api.strings.return_value = mock_response - result = await async_client.generate_strings(term, 3) + result = await async_client.generate_strings(term, 3, 0) assert result == ["", "a", "aa"] From 75ac4a2b35a465c12e18534d4256fbacfbbad5e7 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:18:33 +0100 Subject: [PATCH 29/47] Update test name --- tests/test_async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 126c5a4..87c7a99 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -158,7 +158,7 @@ async def test_error_handling_timeout_exceeded(async_client): @pytest.mark.asyncio -async def test_error_handling_too_many_strings_to_generate(async_client): +async def test_error_handling_invalid_number_of_strings_to_generate(async_client): error_400 = ApiException(status=400) error_400.body = '{"success": false, "error": "Too many strings", "errorCode": "InvalidNumberOfStringsToGenerate"}' async_client._generate_api.strings.side_effect = error_400 From 0d75cac28d25ec7a5991a33891104731c5b81693 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:37:05 +0100 Subject: [PATCH 30/47] Update generate_strings --- .openapi-generator/FILES | 1 + regexsolver/clients/asynchronous.py | 25 ++++- regexsolver/generated/__init__.py | 2 + regexsolver/generated/models/__init__.py | 1 + .../models/generate_strings_request.py | 12 +- .../models/generate_strings_response.py | 106 ++++++++++++++++++ .../generated/models/strings200_response.py | 6 +- regexsolver/models/term.py | 1 + tests/test_async_client.py | 2 +- 9 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 regexsolver/generated/models/generate_strings_response.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index fe331aa..0860315 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -21,6 +21,7 @@ regexsolver/generated/models/empty200_response.py regexsolver/generated/models/error_response.py regexsolver/generated/models/execution_options.py regexsolver/generated/models/generate_strings_request.py +regexsolver/generated/models/generate_strings_response.py regexsolver/generated/models/length.py regexsolver/generated/models/length200_response.py regexsolver/generated/models/multi_terms_request.py diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 2e7ed81..44fcc33 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -581,28 +581,41 @@ async def repeat( async def generate_strings( self, term: Term, - count: int, + limit: int, offset: int, execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `count` distinct strings matched by 'term', skipping the first 'offset' strings. + """Generates up to `limit` distinct strings matched by 'term', skipping the first 'offset' strings. Args: term: The term to sample generated strings from. - count: The maximum number of unique strings to return. + limit: The maximum number of unique strings to return. offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. Returns: List[str]: A list of strings that match the term. """ + + term_to_use = term._api_model + return_stable_term = False + if term._stable_term is not None: + term_to_use = term._stable_term + else: + return_stable_term = True + request = GenerateStringsRequest( - term=term._api_model, - count=count, + term=term_to_use, + limit=limit, offset=offset, + returnStableTerm=return_stable_term, options=self._build_options(execution_timeout), ) response = await self._execute_with_retry( self._generate_api.strings, generate_strings_request=request ) - return response.data.value + + if response.data.term is not None: + term._stable_term = response.data.term + + return response.data.strings.value diff --git a/regexsolver/generated/__init__.py b/regexsolver/generated/__init__.py index 4180f5d..2466ec2 100644 --- a/regexsolver/generated/__init__.py +++ b/regexsolver/generated/__init__.py @@ -42,6 +42,7 @@ "ErrorResponse", "ExecutionOptions", "GenerateStringsRequest", + "GenerateStringsResponse", "Length", "Length200Response", "MultiTermsRequest", @@ -87,6 +88,7 @@ from regexsolver.generated.models.error_response import ErrorResponse as ErrorResponse from regexsolver.generated.models.execution_options import ExecutionOptions as ExecutionOptions from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest +from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse from regexsolver.generated.models.length import Length as Length from regexsolver.generated.models.length200_response import Length200Response as Length200Response from regexsolver.generated.models.multi_terms_request import MultiTermsRequest as MultiTermsRequest diff --git a/regexsolver/generated/models/__init__.py b/regexsolver/generated/models/__init__.py index aabbb11..5315dc0 100644 --- a/regexsolver/generated/models/__init__.py +++ b/regexsolver/generated/models/__init__.py @@ -25,6 +25,7 @@ from regexsolver.generated.models.error_response import ErrorResponse from regexsolver.generated.models.execution_options import ExecutionOptions from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest +from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse from regexsolver.generated.models.length import Length from regexsolver.generated.models.length200_response import Length200Response from regexsolver.generated.models.multi_terms_request import MultiTermsRequest diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/generated/models/generate_strings_request.py index 15cc5e1..ed9e88c 100644 --- a/regexsolver/generated/models/generate_strings_request.py +++ b/regexsolver/generated/models/generate_strings_request.py @@ -17,7 +17,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictInt +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from regexsolver.generated.models.request_options import RequestOptions @@ -27,13 +27,14 @@ class GenerateStringsRequest(BaseModel): """ - Request to generate up to 'count' distinct strings matched by 'term', skipping the first 'offset' strings. + Request to generate up to 'limit' distinct strings matched by 'term', skipping the first 'offset' strings. """ # noqa: E501 term: Term = Field(description="Source term to generate strings from.") - count: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") + limit: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") offset: StrictInt = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") + return_stable_term: Optional[StrictBool] = Field(default=False, description="If set to true, a stable term is returned. This term can be reused in subsequent calls to guarantee no strings are repeated. If the provided term is already stable, it will not be returned.", alias="returnStableTerm") options: Optional[RequestOptions] = None - __properties: ClassVar[List[str]] = ["term", "count", "offset", "options"] + __properties: ClassVar[List[str]] = ["term", "limit", "offset", "returnStableTerm", "options"] model_config = ConfigDict( populate_by_name=True, @@ -93,8 +94,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, - "count": obj.get("count"), + "limit": obj.get("limit"), "offset": obj.get("offset"), + "returnStableTerm": obj.get("returnStableTerm") if obj.get("returnStableTerm") is not None else False, "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None }) return _obj diff --git a/regexsolver/generated/models/generate_strings_response.py b/regexsolver/generated/models/generate_strings_response.py new file mode 100644 index 0000000..105b46e --- /dev/null +++ b/regexsolver/generated/models/generate_strings_response.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver.generated.models.strings import Strings +from regexsolver.generated.models.term import Term +from typing import Optional, Set +from typing_extensions import Self + +class GenerateStringsResponse(BaseModel): + """ + Response containing distinct strings generated from the requested 'term'. + """ # noqa: E501 + type: StrictStr + term: Optional[Term] = Field(default=None, description="A stable term to use in subsequent calls to guarantee the uniqueness of generated strings. Omitted if 'returnStableTerm' was false in the request, or if the provided term was already stable.") + strings: Strings = Field(description="The generated distinct strings.") + __properties: ClassVar[List[str]] = ["type", "term", "strings"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['generatedStrings']): + raise ValueError("must be one of enum values ('generatedStrings')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of GenerateStringsResponse from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of term + if self.term: + _dict['term'] = self.term.to_dict() + # override the default output from pydantic by calling `to_dict()` of strings + if self.strings: + _dict['strings'] = self.strings.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of GenerateStringsResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, + "strings": Strings.from_dict(obj["strings"]) if obj.get("strings") is not None else None + }) + return _obj + + diff --git a/regexsolver/generated/models/strings200_response.py b/regexsolver/generated/models/strings200_response.py index 2290655..58e2f4d 100644 --- a/regexsolver/generated/models/strings200_response.py +++ b/regexsolver/generated/models/strings200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.strings import Strings +from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse from typing import Optional, Set from typing_extensions import Self @@ -28,7 +28,7 @@ class Strings200Response(BaseModel): Strings200Response """ # noqa: E501 success: StrictBool - data: Strings + data: GenerateStringsResponse __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( @@ -86,7 +86,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "success": obj.get("success"), - "data": Strings.from_dict(obj["data"]) if obj.get("data") is not None else None + "data": GenerateStringsResponse.from_dict(obj["data"]) if obj.get("data") is not None else None }) return _obj diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index d592d78..5e80b69 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -31,6 +31,7 @@ def __init__(self, generated_term: GeneratedTerm): self._empty_string: Optional[bool] = None self._total: Optional[bool] = None self._pattern: Optional[str] = None + self._stable_term: Optional[GeneratedTerm] = None self._dot: Optional[str] = None self._compiled_regex: Optional[Pattern] = None diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 87c7a99..183c65f 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -373,7 +373,7 @@ async def test_repeat(async_client): async def test_generate_strings(async_client): term = Term.regex("a*") mock_response = MagicMock() - mock_response.data.value = ["", "a", "aa"] + mock_response.data.strings.value = ["", "a", "aa"] async_client._generate_api.strings.return_value = mock_response result = await async_client.generate_strings(term, 3, 0) assert result == ["", "a", "aa"] From 008eb0904293781bb5120276efcc8fb1e60af1e4 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sat, 21 Mar 2026 20:08:59 +0100 Subject: [PATCH 31/47] Update generate_strings --- regexsolver/clients/synchronous.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index 84ef3b9..76102db 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -340,15 +340,15 @@ def repeat( def generate_strings( self, term: Term, - count: int, + limit: int, offset: int, execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `count` distinct strings matched by 'term', skipping the first 'offset' strings. + """Generates up to `limit` distinct strings matched by 'term', skipping the first 'offset' strings. Args: term: The term to sample generated strings from. - count: The maximum number of unique strings to return. + limit: The maximum number of unique strings to return. offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. @@ -356,5 +356,5 @@ def generate_strings( List[str]: A list of strings that match the term. """ return self._run_sync( - self._aio.generate_strings(term, count, offset, execution_timeout) + self._aio.generate_strings(term, limit, offset, execution_timeout) ) From c0c38f22043698bba6cfe719b677056da9705401 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:53:10 +0100 Subject: [PATCH 32/47] Update wrapper --- regexsolver/clients/asynchronous.py | 25 ++ regexsolver/clients/synchronous.py | 24 ++ regexsolver/generated/api/compute_api.py | 293 ++++++++++++++++++ .../models/generate_strings_request.py | 4 +- tests/test_async_client.py | 11 + tests/test_sync_client.py | 14 + 6 files changed, 369 insertions(+), 2 deletions(-) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 44fcc33..47e93b4 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -577,6 +577,31 @@ async def repeat( ) return Term(response.data) + async def complement( + self, + term: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the complement of the given term. + + Args: + term: The term to complement. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: The complemented term. + """ + request = TermRequest( + term=term._api_model, + options=self._build_options(execution_timeout, response_format), + ) + response = await self._execute_with_retry( + self._compute_api.complement, term_request=request + ) + return Term(response.data) + # --- GENERATE --- async def generate_strings( self, diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index 76102db..c0ce1e1 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -336,6 +336,30 @@ def repeat( ) ) + def complement( + self, + term: Term, + response_format: Optional[Union[ResponseFormat, str]] = None, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes the complement of the given term. + + Args: + term: The term to complement. + response_format: The return format of the term (any, regex or fair). + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: The complemented term. + """ + return self._run_sync( + self._aio.complement( + term, + response_format=response_format, + execution_timeout=execution_timeout, + ) + ) + # --- GENERATE --- def generate_strings( self, diff --git a/regexsolver/generated/api/compute_api.py b/regexsolver/generated/api/compute_api.py index f6f7d77..c0cf03d 100644 --- a/regexsolver/generated/api/compute_api.py +++ b/regexsolver/generated/api/compute_api.py @@ -18,6 +18,7 @@ from regexsolver.generated.models.concat200_response import Concat200Response from regexsolver.generated.models.multi_terms_request import MultiTermsRequest from regexsolver.generated.models.repeat_request import RepeatRequest +from regexsolver.generated.models.term_request import TermRequest from regexsolver.generated.models.two_terms_request import TwoTermsRequest from regexsolver.generated.api_client import ApiClient, RequestSerialized @@ -38,6 +39,298 @@ def __init__(self, api_client=None) -> None: self.api_client = api_client + @validate_call + async def complement( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Complement + + Computes the complement of the given term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._complement_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def complement_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Complement + + Computes the complement of the given term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._complement_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def complement_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Complement + + Computes the complement of the given term. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._complement_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse", + '401': "ErrorResponse", + '403': "ErrorResponse", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _complement_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/complement', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call async def concat( self, diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/generated/models/generate_strings_request.py index ed9e88c..562232b 100644 --- a/regexsolver/generated/models/generate_strings_request.py +++ b/regexsolver/generated/models/generate_strings_request.py @@ -17,7 +17,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt +from pydantic import BaseModel, ConfigDict, Field, StrictBool from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from regexsolver.generated.models.request_options import RequestOptions @@ -31,7 +31,7 @@ class GenerateStringsRequest(BaseModel): """ # noqa: E501 term: Term = Field(description="Source term to generate strings from.") limit: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") - offset: StrictInt = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") + offset: Annotated[int, Field(strict=True, ge=0)] = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") return_stable_term: Optional[StrictBool] = Field(default=False, description="If set to true, a stable term is returned. This term can be reused in subsequent calls to guarantee no strings are repeated. If the provided term is already stable, it will not be returned.", alias="returnStableTerm") options: Optional[RequestOptions] = None __properties: ClassVar[List[str]] = ["term", "limit", "offset", "returnStableTerm", "options"] diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 183c65f..6db4e69 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -369,6 +369,17 @@ async def test_repeat(async_client): assert result.value == "a{2,3}" +@pytest.mark.asyncio +async def test_complement(async_client): + term = Term.regex(".*a.*") + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = "[^a].*" + async_client._compute_api.complement.return_value = mock_response + result = await async_client.complement(term) + assert result.value == "[^a].*" + + @pytest.mark.asyncio async def test_generate_strings(async_client): term = Term.regex("a*") diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index 5bc6c86..15bf16b 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -40,3 +40,17 @@ def test_sync_client_union(): client._aio.union.assert_called_once_with( term1, term2, response_format=None, execution_timeout=None ) + + +def test_sync_client_complement(): + with RegexSolverClient(api_token="test-token") as client: + mock_result_term = Term.regex("[^a].*") + client._aio.complement = AsyncMock(return_value=mock_result_term) + + term = Term.regex(".*a.*") + result = client.complement(term) + + assert result == mock_result_term + client._aio.complement.assert_called_once_with( + term, response_format=None, execution_timeout=None + ) From 8c3441aaff9db5a7330b2b8ce89e82cbbe76377a Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:28:34 +0100 Subject: [PATCH 33/47] Update README.md --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2566dec..c994f54 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ print(pattern) # de(abc|de|fg)+ For high-performance applications, use the asynchronous client. ```python +import asyncio +from regexsolver import AsyncRegexSolverClient, Term + async def main(): async with AsyncRegexSolverClient("YOUR_API_TOKEN") as client: term1 = Term.regex(r"(abc|de|fg){2,}") @@ -68,8 +71,9 @@ The API can handle terms in two formats: - `fair`: FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine By default, the engine returns whatever the operation produces, with no extra convertion. Override with `response_format`: - ```python +from regexsolver import ResponseFormat + term1 = Term.regex(r"abcde") term2 = Term.regex(r"de") @@ -89,13 +93,15 @@ Regardless of the format, you can always call `get_pattern()` to obtain the rege Set a server-side compute timeout in milliseconds with `execution_timeout`: ```python +from regexsolver.exceptions import TimeoutExceededError + # Limit the server-side compute time to 100 ms try: term1 = Term.regex(r".*ab.*c(de|fg).*dab.*c(de|fg).*ab.*c(de|fg).*dab.*c") term2 = Term.regex(r".*abc.*") res = client.difference(term1, term2, execution_timeout=100) -except TimeoutExceeded as error: +except TimeoutExceededError as error: print(error) # The API returned the following error: The operation took too much time. ``` @@ -123,6 +129,7 @@ Timeout is best effort. The exact time is not guaranteed. | Method | Return | Description | | -------- | ------- | ------- | +| `client.complement(t)` | `Term` | Computes the complement of the given term. | | `client.concat(*terms)` | `Term` | Concatenates multiple terms in order. | | `client.difference(t1, t2)` | `Term` | Computes the difference `t1 - t2`. | | `client.intersection(*terms)` | `Term` | Computes the intersection of the given terms. | @@ -133,7 +140,7 @@ Timeout is best effort. The exact time is not guaranteed. | Method | Return | Description | | -------- | ------- | ------- | -| `client.generate_strings(t, count)` | `List[str]` | Generates up to `count` unique example strings matched by `t`. | +| `client.generate_strings(t, limit, offset)` | `List[str]` | Generates up to `limit` unique strings matched by `t`, skipping the first `offset` strings. | ## Cross-Language Support From df78f88f7cd674258479ddd95d17129e602a5f02 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:33:00 +0100 Subject: [PATCH 34/47] Update README.md --- README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c994f54..9e5d804 100644 --- a/README.md +++ b/README.md @@ -115,32 +115,32 @@ Timeout is best effort. The exact time is not guaranteed. | Method | Return | Description | | -------- | ------- | ------- | -| `client.equivalent(t1, t2)` | `bool` | `True` if `t1` and `t2` accept exactly the same language. | -| `client.get_cardinality(t)` | `Cardinality` | Returns the number of possible matched strings. | -| `client.get_dot(t)` | `str` | Returns a Graphviz DOT representation of the automaton. | -| `client.get_length(t)` | `Length` | Returns the minimum and maximum length of matched strings. | -| `client.get_pattern(t)` | `str` | Returns a regular expression pattern for the term. | -| `client.is_empty(t)` | `bool` | `True` if the term matches no string. | -| `client.is_empty_string(t)` | `bool` | `True` if the term matches only the empty string. | -| `client.is_total(t)` | `bool` | `True` if the term matches all possible strings. | -| `client.subset(t1, t2)` | `bool` | `True` if every string matched by `t1` is also matched by `t2`. | +| `client.equivalent(term1, term2)` | `bool` | `True` if `term1` and `term2` accept exactly the same language. | +| `client.get_cardinality(term)` | `Cardinality` | Returns the number of possible matched strings. | +| `client.get_dot(term)` | `str` | Returns a Graphviz DOT representation of the automaton. | +| `client.get_length(term)` | `Length` | Returns the minimum and maximum length of matched strings. | +| `client.get_pattern(term)` | `str` | Returns a regular expression pattern for the term. | +| `client.is_empty(term)` | `bool` | `True` if the term matches no string. | +| `client.is_empty_string(term)` | `bool` | `True` if the term matches only the empty string. | +| `client.is_total(term)` | `bool` | `True` if the term matches all possible strings. | +| `client.subset(term1, term2)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | ### Compute | Method | Return | Description | | -------- | ------- | ------- | -| `client.complement(t)` | `Term` | Computes the complement of the given term. | +| `client.complement(term)` | `Term` | Computes the complement of the given term. | | `client.concat(*terms)` | `Term` | Concatenates multiple terms in order. | -| `client.difference(t1, t2)` | `Term` | Computes the difference `t1 - t2`. | +| `client.difference(term1, term2)` | `Term` | Computes the difference `term1 - term2`. | | `client.intersection(*terms)` | `Term` | Computes the intersection of the given terms. | -| `client.repeat(t, min, max)` | `Term` | Computes the repetition of the term between `min` and `max` times. | +| `client.repeat(term, min, max)` | `Term` | Computes the repetition of the term between `min` and `max` times. | | `client.union(*terms)` | `Term` | Computes the union of the given terms. | ### Generate | Method | Return | Description | | -------- | ------- | ------- | -| `client.generate_strings(t, limit, offset)` | `List[str]` | Generates up to `limit` unique strings matched by `t`, skipping the first `offset` strings. | +| `client.generate_strings(term, limit, offset)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. | ## Cross-Language Support From 76b6ccede75bc2cfb95ae6b00fc6142436ddc7a2 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:07:01 +0100 Subject: [PATCH 35/47] small refactoring --- regexsolver/clients/asynchronous.py | 158 ++++++++++++++-------------- regexsolver/models/length.py | 3 + 2 files changed, 84 insertions(+), 77 deletions(-) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 47e93b4..abdb206 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -106,11 +106,8 @@ async def _execute_with_retry(self, api_method, **kwargs): except ApiException as e: if e.status == 429: if retried: - logger.error("Max retries exceeded for 429 Too Many Requests.") - raise TooManyRequestsError( - "Max retries exceeded for 429 Too Many Requests.", - status_code=429, - ) + raise self._map_error(e) + retried = True headers = e.headers or {} retry_after = float(headers.get("Retry-After", 1)) @@ -120,80 +117,87 @@ async def _execute_with_retry(self, api_method, **kwargs): ) await self._rate_limiter.trigger(retry_after) continue - error_msg = e.reason - error_code = None - if e.body: - try: - parsed_error = ErrorResponse.from_json(e.body) - if parsed_error is not None: - error_msg = parsed_error.error - error_code = parsed_error.error_code - else: - error_msg = e.body - except Exception: - error_msg = e.body - error_msg = str(error_msg) if error_msg else "Unknown API Error" - error_code = str(error_code) if error_code else "UnknownError" - logger.error( - f"RegexSolver API request failed with status {e.status}: {error_code}/{error_msg}" - ) - if e.status == 400: - if error_code == "InvalidJson": - raise InvalidJsonError( - error_msg, status_code=e.status, body=e.body - ) from None - elif error_code == "TooManyTerms": - raise TooManyTermsError( - error_msg, status_code=e.status, body=e.body - ) from None - elif error_code == "TimeoutTooLarge": - raise TimeoutTooLargeError( - error_msg, status_code=e.status, body=e.body - ) from None - elif error_code == "TimeoutExceeded": - raise TimeoutExceededError( - error_msg, status_code=e.status, body=e.body - ) from None - elif error_code == "InvalidNumberOfStringsToGenerate": - raise InvalidNumberOfStringsToGenerate( - error_msg, status_code=e.status, body=e.body - ) from None - raise BadRequestError( - error_msg, status_code=e.status, body=e.body - ) from None - elif e.status == 401: - if error_code == "MissingOrMalformedToken": - raise MissingOrMalformedTokenError( - error_msg, status_code=e.status, body=e.body - ) from None - elif error_code == "InvalidToken": - raise InvalidTokenError( - error_msg, status_code=e.status, body=e.body - ) from None - raise UnauthorizedError( - error_msg, status_code=e.status, body=e.body - ) from None - elif e.status == 403: - if error_code == "QuotaExceeded": - raise QuotaExceededError( - error_msg, status_code=e.status, body=e.body - ) from None - raise ForbiddenError( - error_msg, status_code=e.status, body=e.body - ) from None - elif e.status == 404: - raise NotFoundError( - error_msg, status_code=e.status, body=e.body - ) from None - elif e.status == 500: - raise InternalServerError( - error_msg, status_code=e.status, body=e.body - ) from None + raise self._map_error(e) + + def _map_error(self, e: ApiException) -> Exception: + status_code = e.status + error_msg = e.reason + error_code = None + + if e.body: + try: + parsed_error = ErrorResponse.from_json(e.body) + if parsed_error is not None: + error_msg = parsed_error.error + error_code = parsed_error.error_code else: - raise ApiError( - error_msg, status_code=e.status, body=e.body - ) from None + error_msg = e.body + except Exception: + error_msg = e.body + + error_msg = str(error_msg) if error_msg else "Unknown API Error" + error_code = str(error_code) if error_code else "UnknownError" + + logger.error( + f"RegexSolver API request failed with status {status_code}: {error_code}/{error_msg}" + ) + + if status_code == 400: + if error_code == "InvalidJson": + return InvalidJsonError(error_msg, status_code=status_code, body=e.body) + if error_code == "TooManyTerms": + return TooManyTermsError( + error_msg, status_code=status_code, body=e.body + ) + if error_code == "TimeoutTooLarge": + return TimeoutTooLargeError( + error_msg, status_code=status_code, body=e.body + ) + if error_code == "TimeoutExceeded": + return TimeoutExceededError( + error_msg, status_code=status_code, body=e.body + ) + if error_code == "InvalidNumberOfStringsToGenerate": + return InvalidNumberOfStringsToGenerate( + error_msg, status_code=status_code, body=e.body + ) + return BadRequestError(error_msg, status_code=status_code, body=e.body) + + elif status_code == 401: + if error_code == "MissingOrMalformedToken": + return MissingOrMalformedTokenError( + error_msg, status_code=status_code, body=e.body + ) + if error_code == "InvalidToken": + return InvalidTokenError( + error_msg, status_code=status_code, body=e.body + ) + return UnauthorizedError(error_msg, status_code=status_code, body=e.body) + + elif status_code == 403: + if error_code == "QuotaExceeded": + return QuotaExceededError( + error_msg, status_code=status_code, body=e.body + ) + return ForbiddenError(error_msg, status_code=status_code, body=e.body) + + elif status_code == 404: + return NotFoundError(error_msg, status_code=status_code, body=e.body) + + elif status_code == 429: + msg = ( + "Max retries exceeded for 429 Too Many Requests." + if error_msg == "Unknown API Error" + else error_msg + ) + return TooManyRequestsError(msg, status_code=429) + + elif status_code == 500: + return InternalServerError(error_msg, status_code=status_code, body=e.body) + + else: + return ApiError(error_msg, status_code=status_code, body=e.body) def _build_options( self, diff --git a/regexsolver/models/length.py b/regexsolver/models/length.py index 3dc5f9f..32066d6 100644 --- a/regexsolver/models/length.py +++ b/regexsolver/models/length.py @@ -30,3 +30,6 @@ def is_total(self) -> Optional[bool]: return False else: return None + + def is_infinite(self) -> bool: + return self.min is not None and self.max is None From ed506e29d97fc3ccb16b4540556ce8e178bf20f9 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:49:00 +0200 Subject: [PATCH 36/47] Update library --- regexsolver/clients/asynchronous.py | 91 +++++----- regexsolver/models/cardinality.py | 28 +++- regexsolver/models/length.py | 13 ++ regexsolver/models/term.py | 252 ++++++++++++---------------- tests/test_async_client.py | 26 ++- tests/test_models.py | 100 ++++++++--- tests/test_sync_client.py | 29 ++++ 7 files changed, 311 insertions(+), 228 deletions(-) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index abdb206..6b0cd76 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -1,7 +1,7 @@ import asyncio import logging import weakref -from typing import List, Optional, Union +from typing import List, Optional from regexsolver.clients.rate_limiter import get_rate_limiter from regexsolver.exceptions import ( @@ -38,7 +38,7 @@ TermRequest, TwoTermsRequest, ) -from regexsolver.models.cardinality import BigInteger, Infinite, Integer +from regexsolver.models.cardinality import Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import Term @@ -202,7 +202,7 @@ def _map_error(self, e: ApiException) -> Exception: def _build_options( self, execution_timeout: Optional[int] = None, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, ) -> RequestOptions: options = RequestOptions(schemaVersion=1) if execution_timeout is not None: @@ -214,7 +214,7 @@ def _build_options( # --- ANALYZE --- async def get_cardinality( self, term: Term, execution_timeout: Optional[int] = None - ): + ) -> Cardinality: """Computes how many unique strings the term matches. Args: @@ -228,31 +228,19 @@ async def get_cardinality( return term._cardinality request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.cardinality, term_request=request ) - generated_cardinality = response.data - actual_model = getattr( - generated_cardinality, "actual_instance", generated_cardinality - ) - - c_type = actual_model.type - if c_type == "infinite": - term._cardinality = Infinite() - elif c_type == "bigInteger": - term._cardinality = BigInteger() - elif c_type == "integer": - term._cardinality = Integer(actual_model.value) - else: - raise ValueError(f"Unknown cardinality type: {c_type}") - + term._cardinality = Cardinality.from_dto(response.data) term._set_properties_mixin(term._cardinality) return term._cardinality - async def get_length(self, term: Term, execution_timeout: Optional[int] = None): + async def get_length( + self, term: Term, execution_timeout: Optional[int] = None + ) -> Length: """Computes the minimum and maximum length of strings matched by the term. Args: @@ -266,14 +254,13 @@ async def get_length(self, term: Term, execution_timeout: Optional[int] = None): return term._length request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.length, term_request=request ) - generated_length = response.data - term._length = Length(min=generated_length.min, max=generated_length.max) + term._length = Length.from_dto(response.data) term._set_properties_mixin(term._length) return term._length @@ -291,7 +278,7 @@ async def equivalent( bool: True if they are entirely equivalent, False otherwise. """ request = TwoTermsRequest( - terms=[term1._api_model, term2._api_model], + terms=[term1.to_dto(), term2.to_dto()], options=self._build_options(execution_timeout), ) response = await self._execute_with_retry( @@ -316,7 +303,7 @@ async def subset( bool: True if every string matched by `term_subset` is also matched by `term_superset`. """ request = TwoTermsRequest( - terms=[term_subset._api_model, term_superset._api_model], + terms=[term_subset.to_dto(), term_superset.to_dto()], options=self._build_options(execution_timeout), ) response = await self._execute_with_retry( @@ -339,7 +326,7 @@ async def is_empty( if term._empty is not None: return term._empty request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.empty, term_request=request @@ -365,7 +352,7 @@ async def is_empty_string( if term._empty_string is not None: return term._empty_string request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.empty_string, term_request=request @@ -391,7 +378,7 @@ async def is_total( if term._total is not None: return term._total request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.total, term_request=request @@ -418,7 +405,7 @@ async def get_pattern( if pattern is not None: return pattern request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.pattern, term_request=request @@ -439,7 +426,7 @@ async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> if term._dot is not None: return term._dot request = TermRequest( - term=term._api_model, options=self._build_options(execution_timeout) + term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( self._analyze_api.dot, term_request=request @@ -451,7 +438,7 @@ async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> async def concat( self, *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Concatenates the given terms sequentially. @@ -465,18 +452,18 @@ async def concat( Term: A newly computed concatenated term. """ request = MultiTermsRequest( - terms=[t._api_model for t in terms], + terms=[t.to_dto() for t in terms], options=self._build_options(execution_timeout, response_format), ) response = await self._execute_with_retry( self._compute_api.concat, multi_terms_request=request ) - return Term(response.data) + return Term.from_dto(response.data) async def intersection( self, *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the intersection of the given terms. @@ -490,18 +477,18 @@ async def intersection( Term: A term representing only strings matched by ALL provided terms. """ request = MultiTermsRequest( - terms=[t._api_model for t in terms], + terms=[t.to_dto() for t in terms], options=self._build_options(execution_timeout, response_format), ) response = await self._execute_with_retry( self._compute_api.intersection, multi_terms_request=request ) - return Term(response.data) + return Term.from_dto(response.data) async def union( self, *terms: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the union of the given terms. @@ -515,19 +502,19 @@ async def union( Term: A term representing strings matched by ANY of the provided terms. """ request = MultiTermsRequest( - terms=[t._api_model for t in terms], + terms=[t.to_dto() for t in terms], options=self._build_options(execution_timeout, response_format), ) response = await self._execute_with_retry( self._compute_api.union, multi_terms_request=request ) - return Term(response.data) + return Term.from_dto(response.data) async def difference( self, base_term: Term, excluded_term: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the difference between the two provided terms. @@ -542,20 +529,20 @@ async def difference( Term: A computed difference term. """ request = TwoTermsRequest( - terms=[base_term._api_model, excluded_term._api_model], + terms=[base_term.to_dto(), excluded_term.to_dto()], options=self._build_options(execution_timeout, response_format), ) response = await self._execute_with_retry( self._compute_api.difference, two_terms_request=request ) - return Term(response.data) + return Term.from_dto(response.data) async def repeat( self, term: Term, min_val: int, max_val: Optional[int] = None, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Repeats a term between a minimum and maximum number of times. @@ -571,7 +558,7 @@ async def repeat( Term: A computed repeated term. """ request = RepeatRequest( - term=term._api_model, + term=term.to_dto(), min=min_val, max=max_val, options=self._build_options(execution_timeout, response_format), @@ -579,12 +566,12 @@ async def repeat( response = await self._execute_with_retry( self._compute_api.repeat, repeat_request=request ) - return Term(response.data) + return Term.from_dto(response.data) async def complement( self, term: Term, - response_format: Optional[Union[ResponseFormat, str]] = None, + response_format: Optional[ResponseFormat] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the complement of the given term. @@ -598,13 +585,13 @@ async def complement( Term: The complemented term. """ request = TermRequest( - term=term._api_model, + term=term.to_dto(), options=self._build_options(execution_timeout, response_format), ) response = await self._execute_with_retry( self._compute_api.complement, term_request=request ) - return Term(response.data) + return Term.from_dto(response.data) # --- GENERATE --- async def generate_strings( @@ -626,10 +613,10 @@ async def generate_strings( List[str]: A list of strings that match the term. """ - term_to_use = term._api_model + term_to_use = term.to_dto() return_stable_term = False if term._stable_term is not None: - term_to_use = term._stable_term + term_to_use = term._stable_term.to_dto() else: return_stable_term = True @@ -645,6 +632,6 @@ async def generate_strings( ) if response.data.term is not None: - term._stable_term = response.data.term + term._stable_term = Term.from_dto(response.data.term) return response.data.strings.value diff --git a/regexsolver/models/cardinality.py b/regexsolver/models/cardinality.py index be553a4..2218d3d 100644 --- a/regexsolver/models/cardinality.py +++ b/regexsolver/models/cardinality.py @@ -1,13 +1,37 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, cast +from regexsolver.generated.models import Cardinality as GeneratedCardinality from regexsolver.models.term_properties_mixin import TermPropertiesMixin class Cardinality(TermPropertiesMixin): """Base class representing the number of unique strings matched by a term.""" - pass + @classmethod + def from_dto(cls, dto: GeneratedCardinality) -> "Cardinality": + """Converts a generated API model into a high-level Cardinality object. + + Args: + dto (GeneratedCardinality): The raw model from the generated API. + + Returns: + Cardinality: A specialized instance (Integer, BigInteger, or Infinite). + + Raises: + ValueError: If the DTO contains an unknown cardinality type. + """ + actual_model = getattr(dto, "actual_instance", dto) + c_type = actual_model.type + + if c_type == "infinite": + return Infinite() + elif c_type == "bigInteger": + return BigInteger() + elif c_type == "integer": + return Integer(actual_model.value) + else: + raise ValueError(f"Unknown cardinality type: {c_type}") def __repr__(self) -> str: return "" diff --git a/regexsolver/models/length.py b/regexsolver/models/length.py index 32066d6..39a6972 100644 --- a/regexsolver/models/length.py +++ b/regexsolver/models/length.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from typing import Optional +from regexsolver.generated.models import Length as GeneratedLength from regexsolver.models.term_properties_mixin import TermPropertiesMixin @@ -16,6 +17,18 @@ class Length(TermPropertiesMixin): min: Optional[int] max: Optional[int] + @classmethod + def from_dto(cls, dto: GeneratedLength) -> "Length": + """Converts a generated API model into a high-level Length object. + + Args: + dto (GeneratedLength): The raw model from the generated API. + + Returns: + Length: A high-level instance representing the min/max limits. + """ + return cls(min=dto.min, max=dto.max) + def __repr__(self) -> str: return f"" diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index 5e80b69..78ea456 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -1,6 +1,7 @@ import re +from abc import ABC, abstractmethod from re import Pattern -from typing import Optional, Union, cast +from typing import Any, Optional from regexsolver.generated.models import Term as GeneratedTerm from regexsolver.generated.models.term_fair import TermFair @@ -10,39 +11,54 @@ from regexsolver.models.term_properties_mixin import TermPropertiesMixin -class Term: - """Represents a mathematical term (Regex or FAIR) on which operations can be performed. +class Term(ABC): + """Represents a mathematical term (Regex or FAIR) on which operations can be performed.""" - This is a pure data model that holds the underlying pattern and caches - computed properties (like length and cardinality) to minimize network I/O. - """ - - def __init__(self, generated_term: GeneratedTerm): - """Initializes a new Term instance wrapping the generated API model. - - Args: - generated_term (GeneratedTerm): The raw Pydantic model generated by OpenAPI. - """ - self._inner_term = generated_term + def __init__(self, value: str): + self._value = value + # Shared Cache (Internal) self._cardinality: Optional[Cardinality] = None self._length: Optional[Length] = None self._empty: Optional[bool] = None self._empty_string: Optional[bool] = None self._total: Optional[bool] = None self._pattern: Optional[str] = None - self._stable_term: Optional[GeneratedTerm] = None self._dot: Optional[str] = None + self._stable_term: Optional["Term"] = None + self._compiled_regex: Optional[Pattern] = None - @property - def _actual_model(self) -> Union[TermRegex, TermFair]: - """Extracts the underlying concrete model from the generated OpenAPI oneOf wrapper.""" - actual = getattr(self._inner_term, "actual_instance", self._inner_term) - return cast(Union[TermRegex, TermFair], actual) + @abstractmethod + def get_pattern(self) -> Optional[str]: + pass + + @abstractmethod + def get_fair(self) -> Optional[str]: + pass + + @abstractmethod + def to_dto(self) -> GeneratedTerm: + pass + + @abstractmethod + def serialize(self) -> str: + pass + + @classmethod + def regex(cls, pattern: str) -> "Term": + return RegexTerm(pattern) + + @classmethod + def fair(cls, payload: str) -> "Term": + return FairTerm(payload) + + # --- Shared Behavior --- + + def get_value(self) -> str: + return self._value def _set_properties_mixin(self, properties_mixin: TermPropertiesMixin): - """Updates internal cached properties based on trait inferences.""" empty = properties_mixin.is_empty() if empty is not None: self._empty = empty @@ -55,147 +71,99 @@ def _set_properties_mixin(self, properties_mixin: TermPropertiesMixin): if total is not None: self._total = total - @property - def type(self) -> str: - """str: The format type of the underlying term (e.g., 'regex' or 'fair').""" - return self._actual_model.type - - @property - def value(self) -> str: - """str: The raw string value of the term (the pattern or FAIR payload).""" - return self._actual_model.value - - @property - def _api_model(self) -> GeneratedTerm: - """GeneratedTerm: Returns the raw generated model to be used in API requests.""" - return self._inner_term - - @classmethod - def fair(cls, fair: str) -> "Term": - """Creates a new Term from a Fast Automaton Internal Representation (FAIR) string. + def is_match(self, string: str) -> bool: + """Client-side matching implementation.""" + pattern = self.get_pattern() + if pattern is None: + raise RuntimeError( + "The regex pattern of this term is not defined yet, call get_pattern() on the client to set it." + ) - Args: - fair: The FAIR encoded payload. + if self._compiled_regex is None: + try: + self._compiled_regex = re.compile(rf"\A(?:{pattern})\Z", re.DOTALL) + except re.error as e: + raise ValueError( + f"Invalid regular expression for Python's `re` engine: {pattern}" + ) from e - Returns: - Term: A new Term instance representing the FAIR payload. - """ - gen_term = GeneratedTerm(TermFair(type="fair", value=fair)) - return cls(gen_term) + return self._compiled_regex.match(string) is not None @classmethod - def regex(cls, pattern: str) -> "Term": - """Creates a new Term from a regular expression string. - - Args: - pattern: A valid regular expression string. - - Returns: - Term: A new Term instance representing the regex. - """ - gen_term = GeneratedTerm(TermRegex(type="regex", value=pattern)) - return cls(gen_term) + def deserialize(cls, serialized: str) -> Optional["Term"]: + if not serialized or "=" not in serialized: + return None - def get_fair(self) -> Optional[str]: - """Retrieves the FAIR payload if the term was explicitly constructed as one. + index = serialized.find("=") + type_str = serialized[:index] + val = serialized[index + 1 :] - Returns: - Optional[str]: The FAIR payload string, or None if the term is a regex. - """ - if self.type == "fair": - return self.value - return None + if type_str.lower() == "regex": + return cls.regex(val) + elif type_str.lower() == "fair": + return cls.fair(val) - def get_pattern(self) -> Optional[str]: - """Retrieves the term pattern if the term was explicitly constructed as a regex, or if the pattern was previously computed with the client. - - Returns: - Optional[str]: The term pattern as a string, or None if the term is a FAIR and the pattern was not previously computed. - """ - if self.type == "regex": - return self.value - elif self._pattern is not None: - return self._pattern return None - def is_match(self, string: str) -> Optional[bool]: - """Evaluates if a string matches the term using Python's native `re` module. + @classmethod + def from_dto(cls, dto: GeneratedTerm) -> "Term": + actual_instance = dto.actual_instance + if actual_instance is None: + raise RuntimeError("Invalid Term DTO provided.") + if actual_instance.type == "regex": + return cls.regex(actual_instance.value) + else: + return cls.fair(actual_instance.value) + + # --- Shared Getters/Setters --- + + def get_cached_stable_term(self) -> Optional["Term"]: + return self._stable_term + + def set_cached_stable_term(self, stable_term: Optional["Term"]): + self._stable_term = stable_term + + def __eq__(self, other: Any) -> bool: + if self is other: + return True + if not isinstance(other, Term): + return False + return self.serialize() == other.serialize() - Note: The RegexSolver engine is designed for pattern analysis and - computation, not string evaluation. Therefore, this string matching - feature is executed entirely client-side. + def __hash__(self) -> int: + return hash(self.serialize()) - This method strictly enforces RegexSolver's language rules - by anchoring the expression (requiring a full string match) and allowing - the dot ('.') to match line feeds. The compiled regular expression is - cached on the instance for high-performance repeated matching. + def __str__(self) -> str: + return self.serialize() - Args: - string (str): The string to test against the term's pattern. + def __repr__(self) -> str: + return self.serialize() - Returns: - Optional[bool]: True if the string exactly matches, False if it does not, - or None if the term's pattern is currently unknown (e.g., it is a FAIR - term whose pattern hasn't been computed by the client yet). - Raises: - ValueError: If the term's pattern contains syntax supported by RegexSolver - but unsupported by Python's native `re` engine. - """ - pattern = self.get_pattern() - if pattern is None: - return None +class RegexTerm(Term): + def get_pattern(self) -> Optional[str]: + return self.get_value() - if self._compiled_regex is None: - try: - self._compiled_regex = re.compile(pattern, flags=re.DOTALL) - except re.error as e: - raise ValueError( - f"Pattern '{pattern}' cannot be evaluated by Python's re module: {e}" - ) + def get_fair(self) -> Optional[str]: + stable = self.get_cached_stable_term() + return stable.get_fair() if stable else None - return self._compiled_regex.fullmatch(string) is not None + def to_dto(self) -> GeneratedTerm: + return GeneratedTerm(TermRegex(type="regex", value=self.get_value())) def serialize(self) -> str: - """Serializes the Term into a portable string format. - - Returns: - str: The serialized string in the format 'type=value' (e.g., 'regex=[a-z]'). - """ - actual_model = self._actual_model - return f"{actual_model.type}={actual_model.value}" - - @staticmethod - def deserialize(string: str) -> Optional["Term"]: - """Reconstructs a Term instance from a serialized string. + return f"regex={self.get_value()}" - Args: - string: A string previously generated by `Term.serialize()`. - Returns: - Optional[Term]: The parsed Term instance, or None if parsing fails. - """ - if not string or "=" not in string: - return None - prefix, value = string.split("=", 1) - if prefix == "regex": - return Term.regex(value) - elif prefix == "fair": - return Term.fair(value) - return None - - def __str__(self) -> str: - """Returns the serialized representation of the Term.""" - return self.serialize() +class FairTerm(Term): + def get_pattern(self) -> Optional[str]: + return self._pattern - def __eq__(self, other: object) -> bool: - if isinstance(other, Term): - return self.type == other.type and self.value == other.value - return False + def get_fair(self) -> Optional[str]: + return self.get_value() - def __hash__(self) -> int: - """Generates a hash based on the serialized string representation.""" - return hash(self.serialize()) + def to_dto(self) -> GeneratedTerm: + return GeneratedTerm(TermFair(type="fair", value=self.get_value())) - def __repr__(self) -> str: - return f"" + def serialize(self) -> str: + return f"fair={self.get_value()}" diff --git a/tests/test_async_client.py b/tests/test_async_client.py index 6db4e69..e008947 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -61,6 +61,20 @@ async def test_get_cardinality_infinite(async_client): assert isinstance(result, Infinite) +@pytest.mark.asyncio +async def test_get_cardinality_big_integer(async_client): + term = Term.regex(".{100}") + + mock_response = MagicMock() + mock_response.data.actual_instance.type = "bigInteger" + async_client._analyze_api.cardinality.return_value = mock_response + + from regexsolver import BigInteger + + result = await async_client.get_cardinality(term) + assert isinstance(result, BigInteger) + + @pytest.mark.asyncio async def test_get_length(async_client): term = Term.regex("abc") @@ -101,7 +115,7 @@ async def test_compute_union(async_client): result = await async_client.union(term1, term2) assert isinstance(result, Term) - assert result.value == "a|b" + assert result.get_value() == "a|b" @pytest.mark.asyncio @@ -331,7 +345,7 @@ async def test_concat(async_client): mock_response.data.actual_instance.value = "ab" async_client._compute_api.concat.return_value = mock_response result = await async_client.concat(term1, term2) - assert result.value == "ab" + assert result.get_value() == "ab" @pytest.mark.asyncio @@ -343,7 +357,7 @@ async def test_intersection(async_client): mock_response.data.actual_instance.value = "ab" async_client._compute_api.intersection.return_value = mock_response result = await async_client.intersection(term1, term2) - assert result.value == "ab" + assert result.get_value() == "ab" @pytest.mark.asyncio @@ -355,7 +369,7 @@ async def test_difference(async_client): mock_response.data.actual_instance.value = "a" async_client._compute_api.difference.return_value = mock_response result = await async_client.difference(term1, term2) - assert result.value == "a" + assert result.get_value() == "a" @pytest.mark.asyncio @@ -366,7 +380,7 @@ async def test_repeat(async_client): mock_response.data.actual_instance.value = "a{2,3}" async_client._compute_api.repeat.return_value = mock_response result = await async_client.repeat(term, 2, 3) - assert result.value == "a{2,3}" + assert result.get_value() == "a{2,3}" @pytest.mark.asyncio @@ -377,7 +391,7 @@ async def test_complement(async_client): mock_response.data.actual_instance.value = "[^a].*" async_client._compute_api.complement.return_value = mock_response result = await async_client.complement(term) - assert result.value == "[^a].*" + assert result.get_value() == "[^a].*" @pytest.mark.asyncio diff --git a/tests/test_models.py b/tests/test_models.py index f163fe4..4884872 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,20 +1,45 @@ +import pytest + +from regexsolver.generated.models import Cardinality as GeneratedCardinality +from regexsolver.generated.models import ( + CardinalityBigInteger, + CardinalityInfinite, + CardinalityInteger, + TermFair, + TermRegex, +) +from regexsolver.generated.models import Length as GeneratedLength from regexsolver.generated.models import Term as GeneratedTerm -from regexsolver.models.cardinality import BigInteger, Infinite, Integer +from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer from regexsolver.models.length import Length -from regexsolver.models.term import Term +from regexsolver.models.term import FairTerm, RegexTerm, Term def test_term_creation_regex(): term = Term.regex("abc") - assert term.type == "regex" - assert term.value == "abc" - assert isinstance(term._api_model, GeneratedTerm) + assert isinstance(term, RegexTerm) + assert term.get_value() == "abc" + assert isinstance(term.to_dto(), GeneratedTerm) def test_term_creation_fair(): term = Term.fair("fair_payload") - assert term.type == "fair" - assert term.value == "fair_payload" + assert isinstance(term, FairTerm) + assert term.get_value() == "fair_payload" + + +def test_term_from_dto_regex(): + gen_term = GeneratedTerm(TermRegex(type="regex", value="abc")) + term = Term.from_dto(gen_term) + assert isinstance(term, RegexTerm) + assert term.get_value() == "abc" + + +def test_term_from_dto_fair(): + gen_term = GeneratedTerm(TermFair(type="fair", value="payload")) + term = Term.from_dto(gen_term) + assert isinstance(term, FairTerm) + assert term.get_value() == "payload" def test_cardinality_integer(): @@ -26,6 +51,25 @@ def test_cardinality_integer(): assert repr(c) == "" +def test_cardinality_from_dto_integer(): + gen_card = GeneratedCardinality(CardinalityInteger(type="integer", value=10)) + c = Cardinality.from_dto(gen_card) + assert isinstance(c, Integer) + assert c.value == 10 + + +def test_cardinality_from_dto_big_integer(): + gen_card = GeneratedCardinality(CardinalityBigInteger(type="bigInteger")) + c = Cardinality.from_dto(gen_card) + assert isinstance(c, BigInteger) + + +def test_cardinality_from_dto_infinite(): + gen_card = GeneratedCardinality(CardinalityInfinite(type="infinite")) + c = Cardinality.from_dto(gen_card) + assert isinstance(c, Infinite) + + def test_cardinality_integer_zero(): c = Integer(0) assert c.is_empty() is True @@ -54,28 +98,35 @@ def test_cardinality_infinite(): def test_length(): - lenght = Length(min=1, max=5) - assert lenght.min == 1 - assert lenght.max == 5 - assert lenght.is_empty() is False - assert lenght.is_empty_string() is False - assert lenght.is_total() is False - assert repr(lenght) == "" + length = Length(min=1, max=5) + assert length.min == 1 + assert length.max == 5 + assert length.is_empty() is False + assert length.is_empty_string() is False + assert length.is_total() is False + assert repr(length) == "" + + +def test_length_from_dto(): + gen_len = GeneratedLength(type="length", min=1, max=5) + length_obj = Length.from_dto(gen_len) + assert length_obj.min == 1 + assert length_obj.max == 5 def test_length_empty(): - lenght = Length(min=None, max=None) - assert lenght.is_empty() is True + length = Length(min=None, max=None) + assert length.is_empty() is True def test_length_empty_string(): - lenght = Length(min=0, max=0) - assert lenght.is_empty_string() is True + length = Length(min=0, max=0) + assert length.is_empty_string() is True def test_length_total_candidate(): - lenght = Length(min=0, max=None) - assert lenght.is_total() is None # Implementation returns None if it COULD be total + length = Length(min=0, max=None) + assert length.is_total() is None # Implementation returns None if it COULD be total def test_term_properties_caching(): @@ -130,9 +181,6 @@ def test_term_is_match(): assert term.is_match("axxb") is False # anchored (fullmatch) fair_term = Term.fair("payload") - assert fair_term.is_match("abc") is None - - -def test_term_repr(): - term = Term.regex("abc") - assert repr(term) == "" + # Matches the new Java-aligned behavior of throwing an exception + with pytest.raises(RuntimeError, match="not defined yet"): + fair_term.is_match("abc") diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index 15bf16b..ce8cd25 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -54,3 +54,32 @@ def test_sync_client_complement(): client._aio.complement.assert_called_once_with( term, response_format=None, execution_timeout=None ) + + +def test_sync_client_get_length(): + with RegexSolverClient(api_token="test-token") as client: + from regexsolver.models.length import Length + + client._aio.get_length = AsyncMock(return_value=Length(1, 4)) + term = Term.regex("(abc)?d") + result = client.get_length(term) + assert result.min == 1 + assert result.max == 4 + + +def test_sync_client_intersection(): + with RegexSolverClient(api_token="test-token") as client: + mock_result_term = Term.regex("a") + client._aio.intersection = AsyncMock(return_value=mock_result_term) + t1 = Term.regex("a") + t2 = Term.regex("ab") + result = client.intersection(t1, t2) + assert result == mock_result_term + + +def test_sync_client_generate_strings(): + with RegexSolverClient(api_token="test-token") as client: + client._aio.generate_strings = AsyncMock(return_value=["", "a", "aa"]) + term = Term.regex("a*") + result = client.generate_strings(term, 3, 0) + assert result == ["", "a", "aa"] From f3d68ca61ed12e62245145a2a2ae78c7a427d5a2 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:36:22 +0200 Subject: [PATCH 37/47] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e5d804..882ccfe 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Requirements: **Python >= 3.9** ### Synchronous Usage -The synchronous client is the easiest way to get started. +The synchronous client provides a simple, blocking API. ```python from regexsolver import RegexSolverClient, Term @@ -35,7 +35,7 @@ print(pattern) # de(abc|de|fg)+ ### Asynchronous Usage -For high-performance applications, use the asynchronous client. +For non-blocking applications, use the asynchronous client. ```python import asyncio From 5e1a38d977ace6b183e8bf83700edc76ff2ec698 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:45:25 +0200 Subject: [PATCH 38/47] Remove GEMINI.md --- GEMINI.md | 80 ------------------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 GEMINI.md diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index d103957..0000000 --- a/GEMINI.md +++ /dev/null @@ -1,80 +0,0 @@ -# RegexSolver Python API Client - -## Project Overview -RegexSolver Python is a client library for the RegexSolver API, providing tools for advanced regular expression operations such as intersection, union, difference, and equivalence analysis. It supports both synchronous and asynchronous usage. - -### Core Technologies -- **Python**: 3.9+ -- **Pydantic**: Data validation and modeling (used for API models). -- **aiohttp**: Asynchronous HTTP client for API communication. -- **OpenAPI Generator**: Used to generate the underlying API client from a shared specification. -- **Pytest**: For unit and integration testing. - -### Architecture -- `regexsolver/`: Main package. - - `clients/`: Contains `RegexSolverClient` (sync) and `AsyncRegexSolverClient` (async). - - `RegexSolverClient`: Wrapper that manages a background event loop to provide a synchronous API. - - `AsyncRegexSolverClient`: Core implementation using `aiohttp`. - - `models/`: Custom high-level models. - - `Term`: Primary object for Regex or FAIR patterns. Caches computed properties. - - `Cardinality`: Represents `Integer`, `BigInteger`, or `Infinite`. - - `Length`: Represents `min` and `max` matched string lengths. - - `ResponseFormat`: Enum for `any`, `regex`, or `fair`. - - `exceptions.py`: Detailed hierarchy mapping API errors to Python exceptions. - - `generated/`: Code generated by `openapi-generator-cli`. **Do not modify manually.** -- `tests/`: Test suite using `pytest`. - -## Building and Running - -### Installation -To install the project in development mode with all test dependencies: -```bash -pip install -e ".[test]" -``` - -### Running Tests -Tests use `pytest` and `pytest-asyncio`. To run them: -```bash -pytest -``` - -### Linting and Type Checking -The project uses `flake8` for linting and `mypy` for static type checking: -```bash -flake8 regexsolver tests -mypy regexsolver -``` - -### API Generation -The client code in `regexsolver/generated/` is generated from an OpenAPI spec using: -```bash -./generate-api.sh -``` -*Note: This requires `openapi-generator-cli` to be installed and accessible.* - -## Development Conventions - -### Core Domain Logic -- **Term Caching**: `Term` instances cache properties like `cardinality`, `length`, `empty`, `total`, `pattern`, and `dot` once computed by the client to minimize redundant API calls. -- **Client-side Matching**: `Term.is_match(string)` is performed client-side using Python's `re` module. It uses `re.DOTALL` and anchors the match to the full string. -- **Serialization**: `Term.serialize()` produces a `type=value` string (e.g., `regex=[a-z]`), which can be restored via `Term.deserialize(string)`. - -### Request Options -All client methods support: -- `execution_timeout`: Optional integer (milliseconds) to limit engine execution time. -- `response_format`: For `compute` operations, specifies the desired format of the returned `Term`. - -### Exception Handling -The library maps HTTP status codes and API `error_code` values to specific exceptions: -- `400 Bad Request`: `BadRequestError` (Subclasses: `InvalidJsonError`, `TooManyTermsError`, `TimeoutTooLargeError`, `TimeoutExceededError`, `TooManyStringsToGenerateError`). -- `401 Unauthorized`: `UnauthorizedError` (Subclasses: `MissingOrMalformedTokenError`, `InvalidTokenError`). -- `403 Forbidden`: `ForbiddenError` (Subclasses: `QuotaExceededError`). -- `404 Not Found`: `NotFoundError`. -- `429 Too Many Requests`: `TooManyRequestsError` (raised after 5 retries). -- `500 Internal Server Error`: `InternalServerError`. - -### Testing Practices -- Use `pytest` for all tests. -- For async code, use `pytest.mark.asyncio`. -- **Mocking**: Use `unittest.mock` (`MagicMock`, `AsyncMock`) to mock API responses. -- Ensure that both synchronous and asynchronous clients are tested. From cc1ce069bdc8fc880fef6a3e2f8b52efa66153ae Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:36:45 +0200 Subject: [PATCH 39/47] Update library --- .openapi-generator/FILES | 78 ++++++------- README.md | 34 +++--- generate-api.sh | 4 +- regexsolver/_generated/__init__.py | 106 ++++++++++++++++++ regexsolver/_generated/api/__init__.py | 7 ++ .../api/analyze_api.py | 20 ++-- .../api/compute_api.py | 18 +-- .../api/generate_api.py | 16 +-- .../{generated => _generated}/api_client.py | 12 +- .../{generated => _generated}/api_response.py | 0 .../configuration.py | 2 +- .../{generated => _generated}/exceptions.py | 0 regexsolver/_generated/models/__init__.py | 43 +++++++ .../models/boolean.py | 0 .../models/cardinality.py | 6 +- .../models/cardinality200_response.py | 2 +- .../models/cardinality_big_integer.py | 0 .../models/cardinality_infinite.py | 0 .../models/cardinality_integer.py | 0 .../models/concat200_response.py | 2 +- .../models/dot200_response.py | 2 +- .../models/empty200_response.py | 2 +- .../models/error_response.py | 0 .../models/execution_options.py | 0 .../models/generate_strings_request.py | 4 +- .../models/generate_strings_response.py | 4 +- .../models/length.py | 0 .../models/length200_response.py | 2 +- .../models/multi_terms_request.py | 4 +- .../models/repeat_request.py | 4 +- .../models/request_options.py | 4 +- .../models/response_options.py | 0 .../models/string.py | 0 .../models/strings.py | 0 .../models/strings200_response.py | 2 +- .../{generated => _generated}/models/term.py | 4 +- .../models/term_fair.py | 0 .../models/term_regex.py | 0 .../models/term_request.py | 4 +- .../models/two_terms_request.py | 4 +- .../{generated => _generated}/py.typed | 0 regexsolver/{generated => _generated}/rest.py | 2 +- regexsolver/clients/__init__.py | 0 regexsolver/clients/asynchronous.py | 34 +++--- regexsolver/generated/__init__.py | 106 ------------------ regexsolver/generated/api/__init__.py | 7 -- regexsolver/generated/models/__init__.py | 43 ------- regexsolver/models/__init__.py | 0 regexsolver/models/cardinality.py | 2 +- regexsolver/models/length.py | 2 +- regexsolver/models/term.py | 6 +- 51 files changed, 296 insertions(+), 296 deletions(-) create mode 100644 regexsolver/_generated/__init__.py create mode 100644 regexsolver/_generated/api/__init__.py rename regexsolver/{generated => _generated}/api/analyze_api.py (99%) rename regexsolver/{generated => _generated}/api/compute_api.py (99%) rename regexsolver/{generated => _generated}/api/generate_api.py (94%) rename regexsolver/{generated => _generated}/api_client.py (98%) rename regexsolver/{generated => _generated}/api_response.py (100%) rename regexsolver/{generated => _generated}/configuration.py (99%) rename regexsolver/{generated => _generated}/exceptions.py (100%) create mode 100644 regexsolver/_generated/models/__init__.py rename regexsolver/{generated => _generated}/models/boolean.py (100%) rename regexsolver/{generated => _generated}/models/cardinality.py (96%) rename regexsolver/{generated => _generated}/models/cardinality200_response.py (97%) rename regexsolver/{generated => _generated}/models/cardinality_big_integer.py (100%) rename regexsolver/{generated => _generated}/models/cardinality_infinite.py (100%) rename regexsolver/{generated => _generated}/models/cardinality_integer.py (100%) rename regexsolver/{generated => _generated}/models/concat200_response.py (98%) rename regexsolver/{generated => _generated}/models/dot200_response.py (98%) rename regexsolver/{generated => _generated}/models/empty200_response.py (97%) rename regexsolver/{generated => _generated}/models/error_response.py (100%) rename regexsolver/{generated => _generated}/models/execution_options.py (100%) rename regexsolver/{generated => _generated}/models/generate_strings_request.py (97%) rename regexsolver/{generated => _generated}/models/generate_strings_response.py (97%) rename regexsolver/{generated => _generated}/models/length.py (100%) rename regexsolver/{generated => _generated}/models/length200_response.py (98%) rename regexsolver/{generated => _generated}/models/multi_terms_request.py (96%) rename regexsolver/{generated => _generated}/models/repeat_request.py (96%) rename regexsolver/{generated => _generated}/models/request_options.py (95%) rename regexsolver/{generated => _generated}/models/response_options.py (100%) rename regexsolver/{generated => _generated}/models/string.py (100%) rename regexsolver/{generated => _generated}/models/strings.py (100%) rename regexsolver/{generated => _generated}/models/strings200_response.py (96%) rename regexsolver/{generated => _generated}/models/term.py (97%) rename regexsolver/{generated => _generated}/models/term_fair.py (100%) rename regexsolver/{generated => _generated}/models/term_regex.py (100%) rename regexsolver/{generated => _generated}/models/term_request.py (96%) rename regexsolver/{generated => _generated}/models/two_terms_request.py (96%) rename regexsolver/{generated => _generated}/py.typed (100%) rename regexsolver/{generated => _generated}/rest.py (99%) create mode 100644 regexsolver/clients/__init__.py delete mode 100644 regexsolver/generated/__init__.py delete mode 100644 regexsolver/generated/api/__init__.py delete mode 100644 regexsolver/generated/models/__init__.py create mode 100644 regexsolver/models/__init__.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 0860315..5ccaa03 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -1,42 +1,42 @@ .gitignore -regexsolver/generated/__init__.py -regexsolver/generated/api/__init__.py -regexsolver/generated/api/analyze_api.py -regexsolver/generated/api/compute_api.py -regexsolver/generated/api/generate_api.py -regexsolver/generated/api_client.py -regexsolver/generated/api_response.py -regexsolver/generated/configuration.py -regexsolver/generated/exceptions.py -regexsolver/generated/models/__init__.py -regexsolver/generated/models/boolean.py -regexsolver/generated/models/cardinality.py -regexsolver/generated/models/cardinality200_response.py -regexsolver/generated/models/cardinality_big_integer.py -regexsolver/generated/models/cardinality_infinite.py -regexsolver/generated/models/cardinality_integer.py -regexsolver/generated/models/concat200_response.py -regexsolver/generated/models/dot200_response.py -regexsolver/generated/models/empty200_response.py -regexsolver/generated/models/error_response.py -regexsolver/generated/models/execution_options.py -regexsolver/generated/models/generate_strings_request.py -regexsolver/generated/models/generate_strings_response.py -regexsolver/generated/models/length.py -regexsolver/generated/models/length200_response.py -regexsolver/generated/models/multi_terms_request.py -regexsolver/generated/models/repeat_request.py -regexsolver/generated/models/request_options.py -regexsolver/generated/models/response_options.py -regexsolver/generated/models/string.py -regexsolver/generated/models/strings.py -regexsolver/generated/models/strings200_response.py -regexsolver/generated/models/term.py -regexsolver/generated/models/term_fair.py -regexsolver/generated/models/term_regex.py -regexsolver/generated/models/term_request.py -regexsolver/generated/models/two_terms_request.py -regexsolver/generated/py.typed -regexsolver/generated/rest.py +regexsolver/_generated/__init__.py +regexsolver/_generated/api/__init__.py +regexsolver/_generated/api/analyze_api.py +regexsolver/_generated/api/compute_api.py +regexsolver/_generated/api/generate_api.py +regexsolver/_generated/api_client.py +regexsolver/_generated/api_response.py +regexsolver/_generated/configuration.py +regexsolver/_generated/exceptions.py +regexsolver/_generated/models/__init__.py +regexsolver/_generated/models/boolean.py +regexsolver/_generated/models/cardinality.py +regexsolver/_generated/models/cardinality200_response.py +regexsolver/_generated/models/cardinality_big_integer.py +regexsolver/_generated/models/cardinality_infinite.py +regexsolver/_generated/models/cardinality_integer.py +regexsolver/_generated/models/concat200_response.py +regexsolver/_generated/models/dot200_response.py +regexsolver/_generated/models/empty200_response.py +regexsolver/_generated/models/error_response.py +regexsolver/_generated/models/execution_options.py +regexsolver/_generated/models/generate_strings_request.py +regexsolver/_generated/models/generate_strings_response.py +regexsolver/_generated/models/length.py +regexsolver/_generated/models/length200_response.py +regexsolver/_generated/models/multi_terms_request.py +regexsolver/_generated/models/repeat_request.py +regexsolver/_generated/models/request_options.py +regexsolver/_generated/models/response_options.py +regexsolver/_generated/models/string.py +regexsolver/_generated/models/strings.py +regexsolver/_generated/models/strings200_response.py +regexsolver/_generated/models/term.py +regexsolver/_generated/models/term_fair.py +regexsolver/_generated/models/term_regex.py +regexsolver/_generated/models/term_request.py +regexsolver/_generated/models/two_terms_request.py +regexsolver/_generated/py.typed +regexsolver/_generated/rest.py requirements.txt test-requirements.txt diff --git a/README.md b/README.md index 882ccfe..7f98740 100644 --- a/README.md +++ b/README.md @@ -109,38 +109,38 @@ Timeout is best effort. The exact time is not guaranteed. ## API Overview -`RegexSolverClient` and `AsyncRegexSolverClient` exposes the following methods. +`RegexSolverClient` and `AsyncRegexSolverClient` exposes the following methods. All methods accept optional keyword arguments `response_format` and `execution_timeout`. ### Analyze | Method | Return | Description | | -------- | ------- | ------- | -| `client.equivalent(term1, term2)` | `bool` | `True` if `term1` and `term2` accept exactly the same language. | -| `client.get_cardinality(term)` | `Cardinality` | Returns the number of possible matched strings. | -| `client.get_dot(term)` | `str` | Returns a Graphviz DOT representation of the automaton. | -| `client.get_length(term)` | `Length` | Returns the minimum and maximum length of matched strings. | -| `client.get_pattern(term)` | `str` | Returns a regular expression pattern for the term. | -| `client.is_empty(term)` | `bool` | `True` if the term matches no string. | -| `client.is_empty_string(term)` | `bool` | `True` if the term matches only the empty string. | -| `client.is_total(term)` | `bool` | `True` if the term matches all possible strings. | -| `client.subset(term1, term2)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | +| `client.equivalent(term1, term2, **kwargs)` | `bool` | `True` if `term1` and `term2` accept exactly the same language. | +| `client.get_cardinality(term, **kwargs)` | `Cardinality` | Returns the number of possible matched strings. | +| `client.get_dot(term, **kwargs)` | `str` | Returns a Graphviz DOT representation of the automaton. | +| `client.get_length(term, **kwargs)` | `Length` | Returns the minimum and maximum length of matched strings. | +| `client.get_pattern(term, **kwargs)` | `str` | Returns a regular expression pattern for the term. | +| `client.is_empty(term, **kwargs)` | `bool` | `True` if the term matches no string. | +| `client.is_empty_string(term, **kwargs)` | `bool` | `True` if the term matches only the empty string. | +| `client.is_total(term, **kwargs)` | `bool` | `True` if the term matches all possible strings. | +| `client.subset(term1, term2, **kwargs)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | ### Compute | Method | Return | Description | | -------- | ------- | ------- | -| `client.complement(term)` | `Term` | Computes the complement of the given term. | -| `client.concat(*terms)` | `Term` | Concatenates multiple terms in order. | -| `client.difference(term1, term2)` | `Term` | Computes the difference `term1 - term2`. | -| `client.intersection(*terms)` | `Term` | Computes the intersection of the given terms. | -| `client.repeat(term, min, max)` | `Term` | Computes the repetition of the term between `min` and `max` times. | -| `client.union(*terms)` | `Term` | Computes the union of the given terms. | +| `client.complement(term, **kwargs)` | `Term` | Computes the complement of the given term. | +| `client.concat(term1, term2, ..., **kwargs)` | `Term` | Concatenates multiple terms in order. | +| `client.difference(term1, term2, **kwargs)` | `Term` | Computes the difference `term1 - term2`. | +| `client.intersection(term1, term2, ..., **kwargs)` | `Term` | Computes the intersection of the given terms. | +| `client.repeat(term, min, max, **kwargs)` | `Term` | Computes the repetition of the term between `min` and `max` times. | +| `client.union(term1, term2, ..., **kwargs)` | `Term` | Computes the union of the given terms. | ### Generate | Method | Return | Description | | -------- | ------- | ------- | -| `client.generate_strings(term, limit, offset)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. | +| `client.generate_strings(term, limit, offset, **kwargs)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. | ## Cross-Language Support diff --git a/generate-api.sh b/generate-api.sh index 47d49c1..68bf3ee 100644 --- a/generate-api.sh +++ b/generate-api.sh @@ -1,8 +1,8 @@ #!/bin/bash -SPEC_FILE="../shared/openapi.yaml" +SPEC_FILE="../m-lab/shared/openapi.yaml" OUT_DIR="./" -PACKAGE_NAME="regexsolver.generated" +PACKAGE_NAME="regexsolver._generated" echo "Running openapi-generator-cli..." openapi-generator-cli generate \ diff --git a/regexsolver/_generated/__init__.py b/regexsolver/_generated/__init__.py new file mode 100644 index 0000000..94a0f20 --- /dev/null +++ b/regexsolver/_generated/__init__.py @@ -0,0 +1,106 @@ +# coding: utf-8 + +# flake8: noqa + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +__version__ = "1.0.0" + +# Define package exports +__all__ = [ + "AnalyzeApi", + "ComputeApi", + "GenerateApi", + "ApiResponse", + "ApiClient", + "Configuration", + "OpenApiException", + "ApiTypeError", + "ApiValueError", + "ApiKeyError", + "ApiAttributeError", + "ApiException", + "Boolean", + "Cardinality", + "Cardinality200Response", + "CardinalityBigInteger", + "CardinalityInfinite", + "CardinalityInteger", + "Concat200Response", + "Dot200Response", + "Empty200Response", + "ErrorResponse", + "ExecutionOptions", + "GenerateStringsRequest", + "GenerateStringsResponse", + "Length", + "Length200Response", + "MultiTermsRequest", + "RepeatRequest", + "RequestOptions", + "ResponseOptions", + "String", + "Strings", + "Strings200Response", + "Term", + "TermFair", + "TermRegex", + "TermRequest", + "TwoTermsRequest", +] + +# import apis into sdk package +from regexsolver._generated.api.analyze_api import AnalyzeApi as AnalyzeApi +from regexsolver._generated.api.compute_api import ComputeApi as ComputeApi +from regexsolver._generated.api.generate_api import GenerateApi as GenerateApi + +# import ApiClient +from regexsolver._generated.api_response import ApiResponse as ApiResponse +from regexsolver._generated.api_client import ApiClient as ApiClient +from regexsolver._generated.configuration import Configuration as Configuration +from regexsolver._generated.exceptions import OpenApiException as OpenApiException +from regexsolver._generated.exceptions import ApiTypeError as ApiTypeError +from regexsolver._generated.exceptions import ApiValueError as ApiValueError +from regexsolver._generated.exceptions import ApiKeyError as ApiKeyError +from regexsolver._generated.exceptions import ApiAttributeError as ApiAttributeError +from regexsolver._generated.exceptions import ApiException as ApiException + +# import models into sdk package +from regexsolver._generated.models.boolean import Boolean as Boolean +from regexsolver._generated.models.cardinality import Cardinality as Cardinality +from regexsolver._generated.models.cardinality200_response import Cardinality200Response as Cardinality200Response +from regexsolver._generated.models.cardinality_big_integer import CardinalityBigInteger as CardinalityBigInteger +from regexsolver._generated.models.cardinality_infinite import CardinalityInfinite as CardinalityInfinite +from regexsolver._generated.models.cardinality_integer import CardinalityInteger as CardinalityInteger +from regexsolver._generated.models.concat200_response import Concat200Response as Concat200Response +from regexsolver._generated.models.dot200_response import Dot200Response as Dot200Response +from regexsolver._generated.models.empty200_response import Empty200Response as Empty200Response +from regexsolver._generated.models.error_response import ErrorResponse as ErrorResponse +from regexsolver._generated.models.execution_options import ExecutionOptions as ExecutionOptions +from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest +from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse +from regexsolver._generated.models.length import Length as Length +from regexsolver._generated.models.length200_response import Length200Response as Length200Response +from regexsolver._generated.models.multi_terms_request import MultiTermsRequest as MultiTermsRequest +from regexsolver._generated.models.repeat_request import RepeatRequest as RepeatRequest +from regexsolver._generated.models.request_options import RequestOptions as RequestOptions +from regexsolver._generated.models.response_options import ResponseOptions as ResponseOptions +from regexsolver._generated.models.string import String as String +from regexsolver._generated.models.strings import Strings as Strings +from regexsolver._generated.models.strings200_response import Strings200Response as Strings200Response +from regexsolver._generated.models.term import Term as Term +from regexsolver._generated.models.term_fair import TermFair as TermFair +from regexsolver._generated.models.term_regex import TermRegex as TermRegex +from regexsolver._generated.models.term_request import TermRequest as TermRequest +from regexsolver._generated.models.two_terms_request import TwoTermsRequest as TwoTermsRequest + diff --git a/regexsolver/_generated/api/__init__.py b/regexsolver/_generated/api/__init__.py new file mode 100644 index 0000000..aad0bc3 --- /dev/null +++ b/regexsolver/_generated/api/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa + +# import apis into api package +from regexsolver._generated.api.analyze_api import AnalyzeApi +from regexsolver._generated.api.compute_api import ComputeApi +from regexsolver._generated.api.generate_api import GenerateApi + diff --git a/regexsolver/generated/api/analyze_api.py b/regexsolver/_generated/api/analyze_api.py similarity index 99% rename from regexsolver/generated/api/analyze_api.py rename to regexsolver/_generated/api/analyze_api.py index 9bcbf2c..6ec2e2f 100644 --- a/regexsolver/generated/api/analyze_api.py +++ b/regexsolver/_generated/api/analyze_api.py @@ -15,16 +15,16 @@ from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Annotated -from regexsolver.generated.models.cardinality200_response import Cardinality200Response -from regexsolver.generated.models.dot200_response import Dot200Response -from regexsolver.generated.models.empty200_response import Empty200Response -from regexsolver.generated.models.length200_response import Length200Response -from regexsolver.generated.models.term_request import TermRequest -from regexsolver.generated.models.two_terms_request import TwoTermsRequest - -from regexsolver.generated.api_client import ApiClient, RequestSerialized -from regexsolver.generated.api_response import ApiResponse -from regexsolver.generated.rest import RESTResponseType +from regexsolver._generated.models.cardinality200_response import Cardinality200Response +from regexsolver._generated.models.dot200_response import Dot200Response +from regexsolver._generated.models.empty200_response import Empty200Response +from regexsolver._generated.models.length200_response import Length200Response +from regexsolver._generated.models.term_request import TermRequest +from regexsolver._generated.models.two_terms_request import TwoTermsRequest + +from regexsolver._generated.api_client import ApiClient, RequestSerialized +from regexsolver._generated.api_response import ApiResponse +from regexsolver._generated.rest import RESTResponseType class AnalyzeApi: diff --git a/regexsolver/generated/api/compute_api.py b/regexsolver/_generated/api/compute_api.py similarity index 99% rename from regexsolver/generated/api/compute_api.py rename to regexsolver/_generated/api/compute_api.py index c0cf03d..171c5b8 100644 --- a/regexsolver/generated/api/compute_api.py +++ b/regexsolver/_generated/api/compute_api.py @@ -15,15 +15,15 @@ from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Annotated -from regexsolver.generated.models.concat200_response import Concat200Response -from regexsolver.generated.models.multi_terms_request import MultiTermsRequest -from regexsolver.generated.models.repeat_request import RepeatRequest -from regexsolver.generated.models.term_request import TermRequest -from regexsolver.generated.models.two_terms_request import TwoTermsRequest - -from regexsolver.generated.api_client import ApiClient, RequestSerialized -from regexsolver.generated.api_response import ApiResponse -from regexsolver.generated.rest import RESTResponseType +from regexsolver._generated.models.concat200_response import Concat200Response +from regexsolver._generated.models.multi_terms_request import MultiTermsRequest +from regexsolver._generated.models.repeat_request import RepeatRequest +from regexsolver._generated.models.term_request import TermRequest +from regexsolver._generated.models.two_terms_request import TwoTermsRequest + +from regexsolver._generated.api_client import ApiClient, RequestSerialized +from regexsolver._generated.api_response import ApiResponse +from regexsolver._generated.rest import RESTResponseType class ComputeApi: diff --git a/regexsolver/generated/api/generate_api.py b/regexsolver/_generated/api/generate_api.py similarity index 94% rename from regexsolver/generated/api/generate_api.py rename to regexsolver/_generated/api/generate_api.py index f7500a8..b6b83bd 100644 --- a/regexsolver/generated/api/generate_api.py +++ b/regexsolver/_generated/api/generate_api.py @@ -15,12 +15,12 @@ from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Annotated -from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest -from regexsolver.generated.models.strings200_response import Strings200Response +from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest +from regexsolver._generated.models.strings200_response import Strings200Response -from regexsolver.generated.api_client import ApiClient, RequestSerialized -from regexsolver.generated.api_response import ApiResponse -from regexsolver.generated.rest import RESTResponseType +from regexsolver._generated.api_client import ApiClient, RequestSerialized +from regexsolver._generated.api_response import ApiResponse +from regexsolver._generated.rest import RESTResponseType class GenerateApi: @@ -55,7 +55,7 @@ async def strings( ) -> Strings200Response: """Strings - Generate up to 'count' unique strings matched by the term. + Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -128,7 +128,7 @@ async def strings_with_http_info( ) -> ApiResponse[Strings200Response]: """Strings - Generate up to 'count' unique strings matched by the term. + Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -201,7 +201,7 @@ async def strings_without_preload_content( ) -> RESTResponseType: """Strings - Generate up to 'count' unique strings matched by the term. + Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest diff --git a/regexsolver/generated/api_client.py b/regexsolver/_generated/api_client.py similarity index 98% rename from regexsolver/generated/api_client.py rename to regexsolver/_generated/api_client.py index 19d1ede..6fe5271 100644 --- a/regexsolver/generated/api_client.py +++ b/regexsolver/_generated/api_client.py @@ -26,11 +26,11 @@ from typing import Tuple, Optional, List, Dict, Union from pydantic import SecretStr -from regexsolver.generated.configuration import Configuration -from regexsolver.generated.api_response import ApiResponse, T as ApiResponseT -import regexsolver.generated.models -from regexsolver.generated import rest -from regexsolver.generated.exceptions import ( +from regexsolver._generated.configuration import Configuration +from regexsolver._generated.api_response import ApiResponse, T as ApiResponseT +import regexsolver._generated.models +from regexsolver._generated import rest +from regexsolver._generated.exceptions import ( ApiValueError, ApiException, BadRequestException, @@ -458,7 +458,7 @@ def __deserialize(self, data, klass): if klass in self.NATIVE_TYPES_MAPPING: klass = self.NATIVE_TYPES_MAPPING[klass] else: - klass = getattr(regexsolver.generated.models, klass) + klass = getattr(regexsolver._generated.models, klass) if klass in self.PRIMITIVE_TYPES: return self.__deserialize_primitive(data, klass) diff --git a/regexsolver/generated/api_response.py b/regexsolver/_generated/api_response.py similarity index 100% rename from regexsolver/generated/api_response.py rename to regexsolver/_generated/api_response.py diff --git a/regexsolver/generated/configuration.py b/regexsolver/_generated/configuration.py similarity index 99% rename from regexsolver/generated/configuration.py rename to regexsolver/_generated/configuration.py index f385cd4..61f2d2f 100644 --- a/regexsolver/generated/configuration.py +++ b/regexsolver/_generated/configuration.py @@ -234,7 +234,7 @@ def __init__( self.logger = {} """Logging Settings """ - self.logger["package_logger"] = logging.getLogger("regexsolver.generated") + self.logger["package_logger"] = logging.getLogger("regexsolver._generated") self.logger_format = '%(asctime)s %(levelname)s %(message)s' """Log format """ diff --git a/regexsolver/generated/exceptions.py b/regexsolver/_generated/exceptions.py similarity index 100% rename from regexsolver/generated/exceptions.py rename to regexsolver/_generated/exceptions.py diff --git a/regexsolver/_generated/models/__init__.py b/regexsolver/_generated/models/__init__.py new file mode 100644 index 0000000..24378d9 --- /dev/null +++ b/regexsolver/_generated/models/__init__.py @@ -0,0 +1,43 @@ +# coding: utf-8 + +# flake8: noqa +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + +# import models into model package +from regexsolver._generated.models.boolean import Boolean +from regexsolver._generated.models.cardinality import Cardinality +from regexsolver._generated.models.cardinality200_response import Cardinality200Response +from regexsolver._generated.models.cardinality_big_integer import CardinalityBigInteger +from regexsolver._generated.models.cardinality_infinite import CardinalityInfinite +from regexsolver._generated.models.cardinality_integer import CardinalityInteger +from regexsolver._generated.models.concat200_response import Concat200Response +from regexsolver._generated.models.dot200_response import Dot200Response +from regexsolver._generated.models.empty200_response import Empty200Response +from regexsolver._generated.models.error_response import ErrorResponse +from regexsolver._generated.models.execution_options import ExecutionOptions +from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest +from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse +from regexsolver._generated.models.length import Length +from regexsolver._generated.models.length200_response import Length200Response +from regexsolver._generated.models.multi_terms_request import MultiTermsRequest +from regexsolver._generated.models.repeat_request import RepeatRequest +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.response_options import ResponseOptions +from regexsolver._generated.models.string import String +from regexsolver._generated.models.strings import Strings +from regexsolver._generated.models.strings200_response import Strings200Response +from regexsolver._generated.models.term import Term +from regexsolver._generated.models.term_fair import TermFair +from regexsolver._generated.models.term_regex import TermRegex +from regexsolver._generated.models.term_request import TermRequest +from regexsolver._generated.models.two_terms_request import TwoTermsRequest + diff --git a/regexsolver/generated/models/boolean.py b/regexsolver/_generated/models/boolean.py similarity index 100% rename from regexsolver/generated/models/boolean.py rename to regexsolver/_generated/models/boolean.py diff --git a/regexsolver/generated/models/cardinality.py b/regexsolver/_generated/models/cardinality.py similarity index 96% rename from regexsolver/generated/models/cardinality.py rename to regexsolver/_generated/models/cardinality.py index 06276b8..c70ebec 100644 --- a/regexsolver/generated/models/cardinality.py +++ b/regexsolver/_generated/models/cardinality.py @@ -17,9 +17,9 @@ import pprint from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator from typing import Any, List, Optional -from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger -from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite -from regexsolver.generated.models.cardinality_integer import CardinalityInteger +from regexsolver._generated.models.cardinality_big_integer import CardinalityBigInteger +from regexsolver._generated.models.cardinality_infinite import CardinalityInfinite +from regexsolver._generated.models.cardinality_integer import CardinalityInteger from pydantic import StrictStr, Field from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self diff --git a/regexsolver/generated/models/cardinality200_response.py b/regexsolver/_generated/models/cardinality200_response.py similarity index 97% rename from regexsolver/generated/models/cardinality200_response.py rename to regexsolver/_generated/models/cardinality200_response.py index 26dcea0..0351a02 100644 --- a/regexsolver/generated/models/cardinality200_response.py +++ b/regexsolver/_generated/models/cardinality200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.cardinality import Cardinality +from regexsolver._generated.models.cardinality import Cardinality from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/cardinality_big_integer.py b/regexsolver/_generated/models/cardinality_big_integer.py similarity index 100% rename from regexsolver/generated/models/cardinality_big_integer.py rename to regexsolver/_generated/models/cardinality_big_integer.py diff --git a/regexsolver/generated/models/cardinality_infinite.py b/regexsolver/_generated/models/cardinality_infinite.py similarity index 100% rename from regexsolver/generated/models/cardinality_infinite.py rename to regexsolver/_generated/models/cardinality_infinite.py diff --git a/regexsolver/generated/models/cardinality_integer.py b/regexsolver/_generated/models/cardinality_integer.py similarity index 100% rename from regexsolver/generated/models/cardinality_integer.py rename to regexsolver/_generated/models/cardinality_integer.py diff --git a/regexsolver/generated/models/concat200_response.py b/regexsolver/_generated/models/concat200_response.py similarity index 98% rename from regexsolver/generated/models/concat200_response.py rename to regexsolver/_generated/models/concat200_response.py index 2e9288a..71934b4 100644 --- a/regexsolver/generated/models/concat200_response.py +++ b/regexsolver/_generated/models/concat200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/dot200_response.py b/regexsolver/_generated/models/dot200_response.py similarity index 98% rename from regexsolver/generated/models/dot200_response.py rename to regexsolver/_generated/models/dot200_response.py index 095b5a2..e3b4966 100644 --- a/regexsolver/generated/models/dot200_response.py +++ b/regexsolver/_generated/models/dot200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.string import String +from regexsolver._generated.models.string import String from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/empty200_response.py b/regexsolver/_generated/models/empty200_response.py similarity index 97% rename from regexsolver/generated/models/empty200_response.py rename to regexsolver/_generated/models/empty200_response.py index 0539535..85e7b89 100644 --- a/regexsolver/generated/models/empty200_response.py +++ b/regexsolver/_generated/models/empty200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.boolean import Boolean +from regexsolver._generated.models.boolean import Boolean from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/error_response.py b/regexsolver/_generated/models/error_response.py similarity index 100% rename from regexsolver/generated/models/error_response.py rename to regexsolver/_generated/models/error_response.py diff --git a/regexsolver/generated/models/execution_options.py b/regexsolver/_generated/models/execution_options.py similarity index 100% rename from regexsolver/generated/models/execution_options.py rename to regexsolver/_generated/models/execution_options.py diff --git a/regexsolver/generated/models/generate_strings_request.py b/regexsolver/_generated/models/generate_strings_request.py similarity index 97% rename from regexsolver/generated/models/generate_strings_request.py rename to regexsolver/_generated/models/generate_strings_request.py index 562232b..83f71e6 100644 --- a/regexsolver/generated/models/generate_strings_request.py +++ b/regexsolver/_generated/models/generate_strings_request.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictBool from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/generate_strings_response.py b/regexsolver/_generated/models/generate_strings_response.py similarity index 97% rename from regexsolver/generated/models/generate_strings_response.py rename to regexsolver/_generated/models/generate_strings_response.py index 105b46e..a979899 100644 --- a/regexsolver/generated/models/generate_strings_response.py +++ b/regexsolver/_generated/models/generate_strings_response.py @@ -19,8 +19,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator from typing import Any, ClassVar, Dict, List, Optional -from regexsolver.generated.models.strings import Strings -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.strings import Strings +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/length.py b/regexsolver/_generated/models/length.py similarity index 100% rename from regexsolver/generated/models/length.py rename to regexsolver/_generated/models/length.py diff --git a/regexsolver/generated/models/length200_response.py b/regexsolver/_generated/models/length200_response.py similarity index 98% rename from regexsolver/generated/models/length200_response.py rename to regexsolver/_generated/models/length200_response.py index 66fed08..a78c824 100644 --- a/regexsolver/generated/models/length200_response.py +++ b/regexsolver/_generated/models/length200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.length import Length +from regexsolver._generated.models.length import Length from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/multi_terms_request.py b/regexsolver/_generated/models/multi_terms_request.py similarity index 96% rename from regexsolver/generated/models/multi_terms_request.py rename to regexsolver/_generated/models/multi_terms_request.py index 3e96cb0..dcf331c 100644 --- a/regexsolver/generated/models/multi_terms_request.py +++ b/regexsolver/_generated/models/multi_terms_request.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/repeat_request.py b/regexsolver/_generated/models/repeat_request.py similarity index 96% rename from regexsolver/generated/models/repeat_request.py rename to regexsolver/_generated/models/repeat_request.py index 6c78186..516f587 100644 --- a/regexsolver/generated/models/repeat_request.py +++ b/regexsolver/_generated/models/repeat_request.py @@ -19,8 +19,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictInt from typing import Any, ClassVar, Dict, List, Optional -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/request_options.py b/regexsolver/_generated/models/request_options.py similarity index 95% rename from regexsolver/generated/models/request_options.py rename to regexsolver/_generated/models/request_options.py index 883cfff..39661c1 100644 --- a/regexsolver/generated/models/request_options.py +++ b/regexsolver/_generated/models/request_options.py @@ -19,8 +19,8 @@ from pydantic import BaseModel, ConfigDict, Field, StrictInt from typing import Any, ClassVar, Dict, List, Optional -from regexsolver.generated.models.execution_options import ExecutionOptions -from regexsolver.generated.models.response_options import ResponseOptions +from regexsolver._generated.models.execution_options import ExecutionOptions +from regexsolver._generated.models.response_options import ResponseOptions from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/response_options.py b/regexsolver/_generated/models/response_options.py similarity index 100% rename from regexsolver/generated/models/response_options.py rename to regexsolver/_generated/models/response_options.py diff --git a/regexsolver/generated/models/string.py b/regexsolver/_generated/models/string.py similarity index 100% rename from regexsolver/generated/models/string.py rename to regexsolver/_generated/models/string.py diff --git a/regexsolver/generated/models/strings.py b/regexsolver/_generated/models/strings.py similarity index 100% rename from regexsolver/generated/models/strings.py rename to regexsolver/_generated/models/strings.py diff --git a/regexsolver/generated/models/strings200_response.py b/regexsolver/_generated/models/strings200_response.py similarity index 96% rename from regexsolver/generated/models/strings200_response.py rename to regexsolver/_generated/models/strings200_response.py index 58e2f4d..56ffb8e 100644 --- a/regexsolver/generated/models/strings200_response.py +++ b/regexsolver/_generated/models/strings200_response.py @@ -19,7 +19,7 @@ from pydantic import BaseModel, ConfigDict, StrictBool from typing import Any, ClassVar, Dict, List -from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse +from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/term.py b/regexsolver/_generated/models/term.py similarity index 97% rename from regexsolver/generated/models/term.py rename to regexsolver/_generated/models/term.py index 4575d24..028810c 100644 --- a/regexsolver/generated/models/term.py +++ b/regexsolver/_generated/models/term.py @@ -17,8 +17,8 @@ import pprint from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator from typing import Any, List, Optional -from regexsolver.generated.models.term_fair import TermFair -from regexsolver.generated.models.term_regex import TermRegex +from regexsolver._generated.models.term_fair import TermFair +from regexsolver._generated.models.term_regex import TermRegex from pydantic import StrictStr, Field from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self diff --git a/regexsolver/generated/models/term_fair.py b/regexsolver/_generated/models/term_fair.py similarity index 100% rename from regexsolver/generated/models/term_fair.py rename to regexsolver/_generated/models/term_fair.py diff --git a/regexsolver/generated/models/term_regex.py b/regexsolver/_generated/models/term_regex.py similarity index 100% rename from regexsolver/generated/models/term_regex.py rename to regexsolver/_generated/models/term_regex.py diff --git a/regexsolver/generated/models/term_request.py b/regexsolver/_generated/models/term_request.py similarity index 96% rename from regexsolver/generated/models/term_request.py rename to regexsolver/_generated/models/term_request.py index ef3605b..572e213 100644 --- a/regexsolver/generated/models/term_request.py +++ b/regexsolver/_generated/models/term_request.py @@ -19,8 +19,8 @@ from pydantic import BaseModel, ConfigDict from typing import Any, ClassVar, Dict, List, Optional -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/models/two_terms_request.py b/regexsolver/_generated/models/two_terms_request.py similarity index 96% rename from regexsolver/generated/models/two_terms_request.py rename to regexsolver/_generated/models/two_terms_request.py index a422a8d..4eb81d1 100644 --- a/regexsolver/generated/models/two_terms_request.py +++ b/regexsolver/_generated/models/two_terms_request.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, Field from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.term import Term +from regexsolver._generated.models.request_options import RequestOptions +from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self diff --git a/regexsolver/generated/py.typed b/regexsolver/_generated/py.typed similarity index 100% rename from regexsolver/generated/py.typed rename to regexsolver/_generated/py.typed diff --git a/regexsolver/generated/rest.py b/regexsolver/_generated/rest.py similarity index 99% rename from regexsolver/generated/rest.py rename to regexsolver/_generated/rest.py index 2db6fca..93a8f1a 100644 --- a/regexsolver/generated/rest.py +++ b/regexsolver/_generated/rest.py @@ -21,7 +21,7 @@ import aiohttp import aiohttp_retry -from regexsolver.generated.exceptions import ApiException, ApiValueError +from regexsolver._generated.exceptions import ApiException, ApiValueError RESTResponseType = aiohttp.ClientResponse diff --git a/regexsolver/clients/__init__.py b/regexsolver/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 6b0cd76..d5afccf 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -3,6 +3,23 @@ import weakref from typing import List, Optional +from regexsolver._generated import ( + AnalyzeApi, + ApiClient, + ApiException, + ComputeApi, + Configuration, + ErrorResponse, + ExecutionOptions, + GenerateApi, + GenerateStringsRequest, + MultiTermsRequest, + RepeatRequest, + RequestOptions, + ResponseOptions, + TermRequest, + TwoTermsRequest, +) from regexsolver.clients.rate_limiter import get_rate_limiter from regexsolver.exceptions import ( ApiError, @@ -21,23 +38,6 @@ TooManyTermsError, UnauthorizedError, ) -from regexsolver.generated import ( - AnalyzeApi, - ApiClient, - ApiException, - ComputeApi, - Configuration, - ErrorResponse, - ExecutionOptions, - GenerateApi, - GenerateStringsRequest, - MultiTermsRequest, - RepeatRequest, - RequestOptions, - ResponseOptions, - TermRequest, - TwoTermsRequest, -) from regexsolver.models.cardinality import Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat diff --git a/regexsolver/generated/__init__.py b/regexsolver/generated/__init__.py deleted file mode 100644 index 2466ec2..0000000 --- a/regexsolver/generated/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -# coding: utf-8 - -# flake8: noqa - -""" - RegexSolver - - No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - - The version of the OpenAPI document: 1.1.0 - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 - - -__version__ = "1.0.0" - -# Define package exports -__all__ = [ - "AnalyzeApi", - "ComputeApi", - "GenerateApi", - "ApiResponse", - "ApiClient", - "Configuration", - "OpenApiException", - "ApiTypeError", - "ApiValueError", - "ApiKeyError", - "ApiAttributeError", - "ApiException", - "Boolean", - "Cardinality", - "Cardinality200Response", - "CardinalityBigInteger", - "CardinalityInfinite", - "CardinalityInteger", - "Concat200Response", - "Dot200Response", - "Empty200Response", - "ErrorResponse", - "ExecutionOptions", - "GenerateStringsRequest", - "GenerateStringsResponse", - "Length", - "Length200Response", - "MultiTermsRequest", - "RepeatRequest", - "RequestOptions", - "ResponseOptions", - "String", - "Strings", - "Strings200Response", - "Term", - "TermFair", - "TermRegex", - "TermRequest", - "TwoTermsRequest", -] - -# import apis into sdk package -from regexsolver.generated.api.analyze_api import AnalyzeApi as AnalyzeApi -from regexsolver.generated.api.compute_api import ComputeApi as ComputeApi -from regexsolver.generated.api.generate_api import GenerateApi as GenerateApi - -# import ApiClient -from regexsolver.generated.api_response import ApiResponse as ApiResponse -from regexsolver.generated.api_client import ApiClient as ApiClient -from regexsolver.generated.configuration import Configuration as Configuration -from regexsolver.generated.exceptions import OpenApiException as OpenApiException -from regexsolver.generated.exceptions import ApiTypeError as ApiTypeError -from regexsolver.generated.exceptions import ApiValueError as ApiValueError -from regexsolver.generated.exceptions import ApiKeyError as ApiKeyError -from regexsolver.generated.exceptions import ApiAttributeError as ApiAttributeError -from regexsolver.generated.exceptions import ApiException as ApiException - -# import models into sdk package -from regexsolver.generated.models.boolean import Boolean as Boolean -from regexsolver.generated.models.cardinality import Cardinality as Cardinality -from regexsolver.generated.models.cardinality200_response import Cardinality200Response as Cardinality200Response -from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger as CardinalityBigInteger -from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite as CardinalityInfinite -from regexsolver.generated.models.cardinality_integer import CardinalityInteger as CardinalityInteger -from regexsolver.generated.models.concat200_response import Concat200Response as Concat200Response -from regexsolver.generated.models.dot200_response import Dot200Response as Dot200Response -from regexsolver.generated.models.empty200_response import Empty200Response as Empty200Response -from regexsolver.generated.models.error_response import ErrorResponse as ErrorResponse -from regexsolver.generated.models.execution_options import ExecutionOptions as ExecutionOptions -from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest -from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse -from regexsolver.generated.models.length import Length as Length -from regexsolver.generated.models.length200_response import Length200Response as Length200Response -from regexsolver.generated.models.multi_terms_request import MultiTermsRequest as MultiTermsRequest -from regexsolver.generated.models.repeat_request import RepeatRequest as RepeatRequest -from regexsolver.generated.models.request_options import RequestOptions as RequestOptions -from regexsolver.generated.models.response_options import ResponseOptions as ResponseOptions -from regexsolver.generated.models.string import String as String -from regexsolver.generated.models.strings import Strings as Strings -from regexsolver.generated.models.strings200_response import Strings200Response as Strings200Response -from regexsolver.generated.models.term import Term as Term -from regexsolver.generated.models.term_fair import TermFair as TermFair -from regexsolver.generated.models.term_regex import TermRegex as TermRegex -from regexsolver.generated.models.term_request import TermRequest as TermRequest -from regexsolver.generated.models.two_terms_request import TwoTermsRequest as TwoTermsRequest - diff --git a/regexsolver/generated/api/__init__.py b/regexsolver/generated/api/__init__.py deleted file mode 100644 index e5e55a6..0000000 --- a/regexsolver/generated/api/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# flake8: noqa - -# import apis into api package -from regexsolver.generated.api.analyze_api import AnalyzeApi -from regexsolver.generated.api.compute_api import ComputeApi -from regexsolver.generated.api.generate_api import GenerateApi - diff --git a/regexsolver/generated/models/__init__.py b/regexsolver/generated/models/__init__.py deleted file mode 100644 index 5315dc0..0000000 --- a/regexsolver/generated/models/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 - -# flake8: noqa -""" - RegexSolver - - No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - - The version of the OpenAPI document: 1.1.0 - Generated by OpenAPI Generator (https://openapi-generator.tech) - - Do not edit the class manually. -""" # noqa: E501 - -# import models into model package -from regexsolver.generated.models.boolean import Boolean -from regexsolver.generated.models.cardinality import Cardinality -from regexsolver.generated.models.cardinality200_response import Cardinality200Response -from regexsolver.generated.models.cardinality_big_integer import CardinalityBigInteger -from regexsolver.generated.models.cardinality_infinite import CardinalityInfinite -from regexsolver.generated.models.cardinality_integer import CardinalityInteger -from regexsolver.generated.models.concat200_response import Concat200Response -from regexsolver.generated.models.dot200_response import Dot200Response -from regexsolver.generated.models.empty200_response import Empty200Response -from regexsolver.generated.models.error_response import ErrorResponse -from regexsolver.generated.models.execution_options import ExecutionOptions -from regexsolver.generated.models.generate_strings_request import GenerateStringsRequest -from regexsolver.generated.models.generate_strings_response import GenerateStringsResponse -from regexsolver.generated.models.length import Length -from regexsolver.generated.models.length200_response import Length200Response -from regexsolver.generated.models.multi_terms_request import MultiTermsRequest -from regexsolver.generated.models.repeat_request import RepeatRequest -from regexsolver.generated.models.request_options import RequestOptions -from regexsolver.generated.models.response_options import ResponseOptions -from regexsolver.generated.models.string import String -from regexsolver.generated.models.strings import Strings -from regexsolver.generated.models.strings200_response import Strings200Response -from regexsolver.generated.models.term import Term -from regexsolver.generated.models.term_fair import TermFair -from regexsolver.generated.models.term_regex import TermRegex -from regexsolver.generated.models.term_request import TermRequest -from regexsolver.generated.models.two_terms_request import TwoTermsRequest - diff --git a/regexsolver/models/__init__.py b/regexsolver/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/regexsolver/models/cardinality.py b/regexsolver/models/cardinality.py index 2218d3d..2eae12b 100644 --- a/regexsolver/models/cardinality.py +++ b/regexsolver/models/cardinality.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Optional, cast -from regexsolver.generated.models import Cardinality as GeneratedCardinality +from regexsolver._generated.models import Cardinality as GeneratedCardinality from regexsolver.models.term_properties_mixin import TermPropertiesMixin diff --git a/regexsolver/models/length.py b/regexsolver/models/length.py index 39a6972..8fc30c1 100644 --- a/regexsolver/models/length.py +++ b/regexsolver/models/length.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Optional -from regexsolver.generated.models import Length as GeneratedLength +from regexsolver._generated.models import Length as GeneratedLength from regexsolver.models.term_properties_mixin import TermPropertiesMixin diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index 78ea456..55c9436 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -3,9 +3,9 @@ from re import Pattern from typing import Any, Optional -from regexsolver.generated.models import Term as GeneratedTerm -from regexsolver.generated.models.term_fair import TermFair -from regexsolver.generated.models.term_regex import TermRegex +from regexsolver._generated.models import Term as GeneratedTerm +from regexsolver._generated.models.term_fair import TermFair +from regexsolver._generated.models.term_regex import TermRegex from regexsolver.models.cardinality import Cardinality from regexsolver.models.length import Length from regexsolver.models.term_properties_mixin import TermPropertiesMixin From dd309019459b02455344a14191e9db4ef8be545d Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:23:25 +0200 Subject: [PATCH 40/47] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7f98740..c5089e4 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The synchronous client provides a simple, blocking API. ```python from regexsolver import RegexSolverClient, Term -client = RegexSolverClient("YOUR_API_TOKEN") +client = RegexSolverClient("REGEXSOLVER_API_TOKEN") term1 = Term.regex(r"(abc|de|fg){2,}") term2 = Term.regex(r"de.*") @@ -42,7 +42,7 @@ import asyncio from regexsolver import AsyncRegexSolverClient, Term async def main(): - async with AsyncRegexSolverClient("YOUR_API_TOKEN") as client: + async with AsyncRegexSolverClient("REGEXSOLVER_API_TOKEN") as client: term1 = Term.regex(r"(abc|de|fg){2,}") term2 = Term.regex(r"de.*") From 7b288946b0f79d48bdfb99d27d2e1506839e4cfd Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:14:35 +0200 Subject: [PATCH 41/47] Update possible errors --- .openapi-generator/FILES | 3 + generate-api.sh | 0 regexsolver/_generated/__init__.py | 6 + regexsolver/_generated/api/analyze_api.py | 162 +++++++++--------- regexsolver/_generated/api/compute_api.py | 108 ++++++------ regexsolver/_generated/api/generate_api.py | 18 +- regexsolver/_generated/models/__init__.py | 3 + .../_generated/models/error_response.py | 2 +- .../_generated/models/error_response400.py | 101 +++++++++++ .../_generated/models/error_response401.py | 101 +++++++++++ .../_generated/models/error_response403.py | 101 +++++++++++ regexsolver/clients/asynchronous.py | 8 + regexsolver/exceptions.py | 12 ++ 13 files changed, 480 insertions(+), 145 deletions(-) mode change 100644 => 100755 generate-api.sh create mode 100644 regexsolver/_generated/models/error_response400.py create mode 100644 regexsolver/_generated/models/error_response401.py create mode 100644 regexsolver/_generated/models/error_response403.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 5ccaa03..77e044b 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -19,6 +19,9 @@ regexsolver/_generated/models/concat200_response.py regexsolver/_generated/models/dot200_response.py regexsolver/_generated/models/empty200_response.py regexsolver/_generated/models/error_response.py +regexsolver/_generated/models/error_response400.py +regexsolver/_generated/models/error_response401.py +regexsolver/_generated/models/error_response403.py regexsolver/_generated/models/execution_options.py regexsolver/_generated/models/generate_strings_request.py regexsolver/_generated/models/generate_strings_response.py diff --git a/generate-api.sh b/generate-api.sh old mode 100644 new mode 100755 diff --git a/regexsolver/_generated/__init__.py b/regexsolver/_generated/__init__.py index 94a0f20..760a117 100644 --- a/regexsolver/_generated/__init__.py +++ b/regexsolver/_generated/__init__.py @@ -40,6 +40,9 @@ "Dot200Response", "Empty200Response", "ErrorResponse", + "ErrorResponse400", + "ErrorResponse401", + "ErrorResponse403", "ExecutionOptions", "GenerateStringsRequest", "GenerateStringsResponse", @@ -86,6 +89,9 @@ from regexsolver._generated.models.dot200_response import Dot200Response as Dot200Response from regexsolver._generated.models.empty200_response import Empty200Response as Empty200Response from regexsolver._generated.models.error_response import ErrorResponse as ErrorResponse +from regexsolver._generated.models.error_response400 import ErrorResponse400 as ErrorResponse400 +from regexsolver._generated.models.error_response401 import ErrorResponse401 as ErrorResponse401 +from regexsolver._generated.models.error_response403 import ErrorResponse403 as ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions as ExecutionOptions from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse diff --git a/regexsolver/_generated/api/analyze_api.py b/regexsolver/_generated/api/analyze_api.py index 6ec2e2f..6c0ae7f 100644 --- a/regexsolver/_generated/api/analyze_api.py +++ b/regexsolver/_generated/api/analyze_api.py @@ -95,9 +95,9 @@ async def cardinality( _response_types_map: Dict[str, Optional[str]] = { '200': "Cardinality200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -168,9 +168,9 @@ async def cardinality_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Cardinality200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -241,9 +241,9 @@ async def cardinality_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Cardinality200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -387,9 +387,9 @@ async def dot( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -460,9 +460,9 @@ async def dot_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -533,9 +533,9 @@ async def dot_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -679,9 +679,9 @@ async def empty( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -752,9 +752,9 @@ async def empty_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -825,9 +825,9 @@ async def empty_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -971,9 +971,9 @@ async def empty_string( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1044,9 +1044,9 @@ async def empty_string_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1117,9 +1117,9 @@ async def empty_string_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1263,9 +1263,9 @@ async def equivalent( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1336,9 +1336,9 @@ async def equivalent_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1409,9 +1409,9 @@ async def equivalent_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1555,9 +1555,9 @@ async def length( _response_types_map: Dict[str, Optional[str]] = { '200': "Length200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1628,9 +1628,9 @@ async def length_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Length200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1701,9 +1701,9 @@ async def length_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Length200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1847,9 +1847,9 @@ async def pattern( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1920,9 +1920,9 @@ async def pattern_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1993,9 +1993,9 @@ async def pattern_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Dot200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2139,9 +2139,9 @@ async def subset( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2212,9 +2212,9 @@ async def subset_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2285,9 +2285,9 @@ async def subset_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2431,9 +2431,9 @@ async def total( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2504,9 +2504,9 @@ async def total_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -2577,9 +2577,9 @@ async def total_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Empty200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", diff --git a/regexsolver/_generated/api/compute_api.py b/regexsolver/_generated/api/compute_api.py index 171c5b8..98a4170 100644 --- a/regexsolver/_generated/api/compute_api.py +++ b/regexsolver/_generated/api/compute_api.py @@ -94,9 +94,9 @@ async def complement( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -167,9 +167,9 @@ async def complement_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -240,9 +240,9 @@ async def complement_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -386,9 +386,9 @@ async def concat( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -459,9 +459,9 @@ async def concat_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -532,9 +532,9 @@ async def concat_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -678,9 +678,9 @@ async def difference( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -751,9 +751,9 @@ async def difference_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -824,9 +824,9 @@ async def difference_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -970,9 +970,9 @@ async def intersection( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1043,9 +1043,9 @@ async def intersection_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1116,9 +1116,9 @@ async def intersection_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1262,9 +1262,9 @@ async def repeat( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1335,9 +1335,9 @@ async def repeat_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1408,9 +1408,9 @@ async def repeat_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1554,9 +1554,9 @@ async def union( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1627,9 +1627,9 @@ async def union_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -1700,9 +1700,9 @@ async def union_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Concat200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", diff --git a/regexsolver/_generated/api/generate_api.py b/regexsolver/_generated/api/generate_api.py index b6b83bd..ef01fc8 100644 --- a/regexsolver/_generated/api/generate_api.py +++ b/regexsolver/_generated/api/generate_api.py @@ -91,9 +91,9 @@ async def strings( _response_types_map: Dict[str, Optional[str]] = { '200': "Strings200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -164,9 +164,9 @@ async def strings_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '200': "Strings200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", @@ -237,9 +237,9 @@ async def strings_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '200': "Strings200Response", - '400': "ErrorResponse", - '401': "ErrorResponse", - '403': "ErrorResponse", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", '404': "ErrorResponse", '429': "ErrorResponse", '500': "ErrorResponse", diff --git a/regexsolver/_generated/models/__init__.py b/regexsolver/_generated/models/__init__.py index 24378d9..c05b44b 100644 --- a/regexsolver/_generated/models/__init__.py +++ b/regexsolver/_generated/models/__init__.py @@ -23,6 +23,9 @@ from regexsolver._generated.models.dot200_response import Dot200Response from regexsolver._generated.models.empty200_response import Empty200Response from regexsolver._generated.models.error_response import ErrorResponse +from regexsolver._generated.models.error_response400 import ErrorResponse400 +from regexsolver._generated.models.error_response401 import ErrorResponse401 +from regexsolver._generated.models.error_response403 import ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse diff --git a/regexsolver/_generated/models/error_response.py b/regexsolver/_generated/models/error_response.py index 1ff5dc1..fe998e1 100644 --- a/regexsolver/_generated/models/error_response.py +++ b/regexsolver/_generated/models/error_response.py @@ -24,7 +24,7 @@ class ErrorResponse(BaseModel): """ - Standard error payload returned when success is false. + ErrorResponse """ # noqa: E501 success: StrictBool error: StrictStr = Field(description="Human readable error message.") diff --git a/regexsolver/_generated/models/error_response400.py b/regexsolver/_generated/models/error_response400.py new file mode 100644 index 0000000..9b1c95d --- /dev/null +++ b/regexsolver/_generated/models/error_response400.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class ErrorResponse400(BaseModel): + """ + ErrorResponse400 + """ # noqa: E501 + success: StrictBool + error: StrictStr = Field(description="Human readable error message.") + error_code: Optional[StrictStr] = Field(default=None, alias="errorCode") + __properties: ClassVar[List[str]] = ["success", "error", "errorCode"] + + @field_validator('error_code') + def error_code_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(['InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError']): + raise ValueError("must be one of enum values ('InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ErrorResponse400 from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ErrorResponse400 from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "error": obj.get("error"), + "errorCode": obj.get("errorCode") + }) + return _obj + + diff --git a/regexsolver/_generated/models/error_response401.py b/regexsolver/_generated/models/error_response401.py new file mode 100644 index 0000000..5d30374 --- /dev/null +++ b/regexsolver/_generated/models/error_response401.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class ErrorResponse401(BaseModel): + """ + ErrorResponse401 + """ # noqa: E501 + success: StrictBool + error: StrictStr = Field(description="Human readable error message.") + error_code: Optional[StrictStr] = Field(default=None, alias="errorCode") + __properties: ClassVar[List[str]] = ["success", "error", "errorCode"] + + @field_validator('error_code') + def error_code_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(['MissingOrMalformedToken', 'InvalidToken']): + raise ValueError("must be one of enum values ('MissingOrMalformedToken', 'InvalidToken')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ErrorResponse401 from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ErrorResponse401 from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "error": obj.get("error"), + "errorCode": obj.get("errorCode") + }) + return _obj + + diff --git a/regexsolver/_generated/models/error_response403.py b/regexsolver/_generated/models/error_response403.py new file mode 100644 index 0000000..d8bb8ae --- /dev/null +++ b/regexsolver/_generated/models/error_response403.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class ErrorResponse403(BaseModel): + """ + ErrorResponse403 + """ # noqa: E501 + success: StrictBool + error: StrictStr = Field(description="Human readable error message.") + error_code: Optional[StrictStr] = Field(default=None, alias="errorCode") + __properties: ClassVar[List[str]] = ["success", "error", "errorCode"] + + @field_validator('error_code') + def error_code_validate_enum(cls, value): + """Validates the enum""" + if value is None: + return value + + if value not in set(['QuotaExceeded']): + raise ValueError("must be one of enum values ('QuotaExceeded')") + return value + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of ErrorResponse403 from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of ErrorResponse403 from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "error": obj.get("error"), + "errorCode": obj.get("errorCode") + }) + return _obj + + diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index d5afccf..f23288d 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -28,6 +28,8 @@ InternalServerError, InvalidJsonError, InvalidNumberOfStringsToGenerate, + AutomatonTooManyStatesError, + RegexSyntaxError, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, @@ -162,6 +164,12 @@ def _map_error(self, e: ApiException) -> Exception: return InvalidNumberOfStringsToGenerate( error_msg, status_code=status_code, body=e.body ) + if error_code == "AutomatonTooManyStates": + return AutomatonTooManyStatesError( + error_msg, status_code=status_code, body=e.body + ) + if error_code == "RegexSyntaxError": + return RegexSyntaxError(error_msg, status_code=status_code, body=e.body) return BadRequestError(error_msg, status_code=status_code, body=e.body) elif status_code == 401: diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 8e46f6c..f3de5cd 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -62,6 +62,18 @@ class InvalidNumberOfStringsToGenerate(BadRequestError): pass +class AutomatonTooManyStatesError(BadRequestError): + """Raised when the NFA/DFA exceeds the maximum allowed number of states for your current plan.""" + + pass + + +class RegexSyntaxError(BadRequestError): + """Raised when the provided regular expression has invalid syntax.""" + + pass + + class UnauthorizedError(ApiError): """Raised when the API returns a 401 Unauthorized error.""" From af7f0e2a7473863d5e4723e9c681693d149ea513 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:55:19 +0200 Subject: [PATCH 42/47] Update endpoints --- .openapi-generator/FILES | 2 + README.md | 2 + regexsolver/__init__.py | 8 +- regexsolver/_generated/__init__.py | 4 + regexsolver/_generated/api/analyze_api.py | 298 +++++++++++++++- regexsolver/_generated/api/compute_api.py | 322 +++++++++++++++++- regexsolver/_generated/api/generate_api.py | 6 +- regexsolver/_generated/models/__init__.py | 2 + .../models/fair_response_options.py | 87 +++++ .../models/generate_strings_request.py | 8 +- .../models/generate_strings_response.py | 12 +- .../_generated/models/repeat_request.py | 9 +- .../_generated/models/request_options.py | 2 +- .../_generated/models/response_options.py | 10 +- regexsolver/_generated/models/term_fair.py | 14 +- .../_generated/models/term_fair_metadata.py | 87 +++++ regexsolver/_generated/models/term_request.py | 2 +- regexsolver/clients/asynchronous.py | 170 +++++++-- regexsolver/clients/synchronous.py | 81 ++++- regexsolver/exceptions.py | 2 +- regexsolver/models/term.py | 33 +- tests/test_async_client.py | 6 +- tests/test_models.py | 137 +++++++- tests/test_sync_client.py | 4 +- 24 files changed, 1190 insertions(+), 118 deletions(-) create mode 100644 regexsolver/_generated/models/fair_response_options.py create mode 100644 regexsolver/_generated/models/term_fair_metadata.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 77e044b..5d6572d 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -23,6 +23,7 @@ regexsolver/_generated/models/error_response400.py regexsolver/_generated/models/error_response401.py regexsolver/_generated/models/error_response403.py regexsolver/_generated/models/execution_options.py +regexsolver/_generated/models/fair_response_options.py regexsolver/_generated/models/generate_strings_request.py regexsolver/_generated/models/generate_strings_response.py regexsolver/_generated/models/length.py @@ -36,6 +37,7 @@ regexsolver/_generated/models/strings.py regexsolver/_generated/models/strings200_response.py regexsolver/_generated/models/term.py regexsolver/_generated/models/term_fair.py +regexsolver/_generated/models/term_fair_metadata.py regexsolver/_generated/models/term_regex.py regexsolver/_generated/models/term_request.py regexsolver/_generated/models/two_terms_request.py diff --git a/README.md b/README.md index c5089e4..2ec73fd 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Timeout is best effort. The exact time is not guaranteed. | `client.is_empty(term, **kwargs)` | `bool` | `True` if the term matches no string. | | `client.is_empty_string(term, **kwargs)` | `bool` | `True` if the term matches only the empty string. | | `client.is_total(term, **kwargs)` | `bool` | `True` if the term matches all possible strings. | +| `client.is_deterministic(term, **kwargs)` | `bool` | `True` if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated `generate_strings()` calls; call `determinize()` first if this is `False`. | | `client.subset(term1, term2, **kwargs)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | ### Compute @@ -131,6 +132,7 @@ Timeout is best effort. The exact time is not guaranteed. | -------- | ------- | ------- | | `client.complement(term, **kwargs)` | `Term` | Computes the complement of the given term. | | `client.concat(term1, term2, ..., **kwargs)` | `Term` | Concatenates multiple terms in order. | +| `client.determinize(term, **kwargs)` | `Term` | Computes a deterministic FAIR for the given term, suitable for consistent pagination with `generate_strings()`. | | `client.difference(term1, term2, **kwargs)` | `Term` | Computes the difference `term1 - term2`. | | `client.intersection(term1, term2, ..., **kwargs)` | `Term` | Computes the intersection of the given terms. | | `client.repeat(term, min, max, **kwargs)` | `Term` | Computes the repetition of the term between `min` and `max` times. | diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 1a51ad5..b91f1e4 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -2,16 +2,18 @@ from regexsolver.clients.synchronous import RegexSolverClient from regexsolver.exceptions import ( ApiError, + AutomatonTooManyStatesError, BadRequestError, ForbiddenError, InternalServerError, InvalidJsonError, - InvalidNumberOfStringsToGenerate, + InvalidNumberOfStringsToGenerateError, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, QuotaExceededError, RegexSolverError, + RegexSyntaxError, TimeoutExceededError, TimeoutTooLargeError, TooManyRequestsError, @@ -28,6 +30,7 @@ "RegexSolverClient", "Term", "ApiError", + "AutomatonTooManyStatesError", "BadRequestError", "ForbiddenError", "InternalServerError", @@ -37,10 +40,11 @@ "NotFoundError", "QuotaExceededError", "RegexSolverError", + "RegexSyntaxError", "TimeoutExceededError", "TimeoutTooLargeError", "TooManyRequestsError", - "InvalidNumberOfStringsToGenerate", + "InvalidNumberOfStringsToGenerateError", "TooManyTermsError", "UnauthorizedError", "BigInteger", diff --git a/regexsolver/_generated/__init__.py b/regexsolver/_generated/__init__.py index 760a117..177f216 100644 --- a/regexsolver/_generated/__init__.py +++ b/regexsolver/_generated/__init__.py @@ -44,6 +44,7 @@ "ErrorResponse401", "ErrorResponse403", "ExecutionOptions", + "FairResponseOptions", "GenerateStringsRequest", "GenerateStringsResponse", "Length", @@ -57,6 +58,7 @@ "Strings200Response", "Term", "TermFair", + "TermFairMetadata", "TermRegex", "TermRequest", "TwoTermsRequest", @@ -93,6 +95,7 @@ from regexsolver._generated.models.error_response401 import ErrorResponse401 as ErrorResponse401 from regexsolver._generated.models.error_response403 import ErrorResponse403 as ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions as ExecutionOptions +from regexsolver._generated.models.fair_response_options import FairResponseOptions as FairResponseOptions from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse from regexsolver._generated.models.length import Length as Length @@ -106,6 +109,7 @@ from regexsolver._generated.models.strings200_response import Strings200Response as Strings200Response from regexsolver._generated.models.term import Term as Term from regexsolver._generated.models.term_fair import TermFair as TermFair +from regexsolver._generated.models.term_fair_metadata import TermFairMetadata as TermFairMetadata from regexsolver._generated.models.term_regex import TermRegex as TermRegex from regexsolver._generated.models.term_request import TermRequest as TermRequest from regexsolver._generated.models.two_terms_request import TwoTermsRequest as TwoTermsRequest diff --git a/regexsolver/_generated/api/analyze_api.py b/regexsolver/_generated/api/analyze_api.py index 6c0ae7f..a64fae9 100644 --- a/regexsolver/_generated/api/analyze_api.py +++ b/regexsolver/_generated/api/analyze_api.py @@ -332,6 +332,298 @@ def _cardinality_serialize( + @validate_call + async def deterministic( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Empty200Response: + """Deterministic + + Check if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated /generate/strings requests; call /compute/determinize first if this is false. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._deterministic_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def deterministic_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Empty200Response]: + """Deterministic + + Check if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated /generate/strings requests; call /compute/determinize first if this is false. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._deterministic_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def deterministic_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Deterministic + + Check if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated /generate/strings requests; call /compute/determinize first if this is false. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._deterministic_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Empty200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _deterministic_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/analyze/deterministic', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call async def dot( self, @@ -349,7 +641,7 @@ async def dot( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> Dot200Response: - """GraphViz Dot + """Graphviz DOT Build a Graphviz DOT representation of the term's automaton. @@ -422,7 +714,7 @@ async def dot_with_http_info( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> ApiResponse[Dot200Response]: - """GraphViz Dot + """Graphviz DOT Build a Graphviz DOT representation of the term's automaton. @@ -495,7 +787,7 @@ async def dot_without_preload_content( _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """GraphViz Dot + """Graphviz DOT Build a Graphviz DOT representation of the term's automaton. diff --git a/regexsolver/_generated/api/compute_api.py b/regexsolver/_generated/api/compute_api.py index 98a4170..64e6662 100644 --- a/regexsolver/_generated/api/compute_api.py +++ b/regexsolver/_generated/api/compute_api.py @@ -58,7 +58,7 @@ async def complement( ) -> Concat200Response: """Complement - Computes the complement of the given term. + Compute the complement of the given term. :param term_request: (required) :type term_request: TermRequest @@ -131,7 +131,7 @@ async def complement_with_http_info( ) -> ApiResponse[Concat200Response]: """Complement - Computes the complement of the given term. + Compute the complement of the given term. :param term_request: (required) :type term_request: TermRequest @@ -204,7 +204,7 @@ async def complement_without_preload_content( ) -> RESTResponseType: """Complement - Computes the complement of the given term. + Compute the complement of the given term. :param term_request: (required) :type term_request: TermRequest @@ -623,6 +623,298 @@ def _concat_serialize( + @validate_call + async def determinize( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Concat200Response: + """Determinize + + Compute a deterministic FAIR. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._determinize_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def determinize_with_http_info( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Concat200Response]: + """Determinize + + Compute a deterministic FAIR. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._determinize_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def determinize_without_preload_content( + self, + term_request: TermRequest, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Determinize + + Compute a deterministic FAIR. + + :param term_request: (required) + :type term_request: TermRequest + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._determinize_serialize( + term_request=term_request, + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Concat200Response", + '400': "ErrorResponse400", + '401': "ErrorResponse401", + '403': "ErrorResponse403", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _determinize_serialize( + self, + term_request, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + if term_request is not None: + _body_params = term_request + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + # set the HTTP header `Content-Type` + if _content_type: + _header_params['Content-Type'] = _content_type + else: + _default_content_type = ( + self.api_client.select_header_content_type( + [ + 'application/json' + ] + ) + ) + if _default_content_type is not None: + _header_params['Content-Type'] = _default_content_type + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='POST', + resource_path='/compute/determinize', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + + + @validate_call async def difference( self, @@ -642,7 +934,7 @@ async def difference( ) -> Concat200Response: """Difference - Computes the difference between the two provided terms. + Compute the difference between the two given terms. :param two_terms_request: (required) :type two_terms_request: TwoTermsRequest @@ -715,7 +1007,7 @@ async def difference_with_http_info( ) -> ApiResponse[Concat200Response]: """Difference - Computes the difference between the two provided terms. + Compute the difference between the two given terms. :param two_terms_request: (required) :type two_terms_request: TwoTermsRequest @@ -788,7 +1080,7 @@ async def difference_without_preload_content( ) -> RESTResponseType: """Difference - Computes the difference between the two provided terms. + Compute the difference between the two given terms. :param two_terms_request: (required) :type two_terms_request: TwoTermsRequest @@ -934,7 +1226,7 @@ async def intersection( ) -> Concat200Response: """Intersection - Computes the intersection of the given terms. + Compute the intersection of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest @@ -1007,7 +1299,7 @@ async def intersection_with_http_info( ) -> ApiResponse[Concat200Response]: """Intersection - Computes the intersection of the given terms. + Compute the intersection of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest @@ -1080,7 +1372,7 @@ async def intersection_without_preload_content( ) -> RESTResponseType: """Intersection - Computes the intersection of the given terms. + Compute the intersection of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest @@ -1226,7 +1518,7 @@ async def repeat( ) -> Concat200Response: """Repeat - Repeat a term between 'min' and 'max' times. + Repeat a term between `min` and `max` times. :param repeat_request: (required) :type repeat_request: RepeatRequest @@ -1299,7 +1591,7 @@ async def repeat_with_http_info( ) -> ApiResponse[Concat200Response]: """Repeat - Repeat a term between 'min' and 'max' times. + Repeat a term between `min` and `max` times. :param repeat_request: (required) :type repeat_request: RepeatRequest @@ -1372,7 +1664,7 @@ async def repeat_without_preload_content( ) -> RESTResponseType: """Repeat - Repeat a term between 'min' and 'max' times. + Repeat a term between `min` and `max` times. :param repeat_request: (required) :type repeat_request: RepeatRequest @@ -1518,7 +1810,7 @@ async def union( ) -> Concat200Response: """Union - Computes the union of the given terms. + Compute the union of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest @@ -1591,7 +1883,7 @@ async def union_with_http_info( ) -> ApiResponse[Concat200Response]: """Union - Computes the union of the given terms. + Compute the union of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest @@ -1664,7 +1956,7 @@ async def union_without_preload_content( ) -> RESTResponseType: """Union - Computes the union of the given terms. + Compute the union of the given terms. :param multi_terms_request: (required) :type multi_terms_request: MultiTermsRequest diff --git a/regexsolver/_generated/api/generate_api.py b/regexsolver/_generated/api/generate_api.py index ef01fc8..8c10212 100644 --- a/regexsolver/_generated/api/generate_api.py +++ b/regexsolver/_generated/api/generate_api.py @@ -55,7 +55,7 @@ async def strings( ) -> Strings200Response: """Strings - Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -128,7 +128,7 @@ async def strings_with_http_info( ) -> ApiResponse[Strings200Response]: """Strings - Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -201,7 +201,7 @@ async def strings_without_preload_content( ) -> RESTResponseType: """Strings - Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest diff --git a/regexsolver/_generated/models/__init__.py b/regexsolver/_generated/models/__init__.py index c05b44b..cc1b550 100644 --- a/regexsolver/_generated/models/__init__.py +++ b/regexsolver/_generated/models/__init__.py @@ -27,6 +27,7 @@ from regexsolver._generated.models.error_response401 import ErrorResponse401 from regexsolver._generated.models.error_response403 import ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions +from regexsolver._generated.models.fair_response_options import FairResponseOptions from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse from regexsolver._generated.models.length import Length @@ -40,6 +41,7 @@ from regexsolver._generated.models.strings200_response import Strings200Response from regexsolver._generated.models.term import Term from regexsolver._generated.models.term_fair import TermFair +from regexsolver._generated.models.term_fair_metadata import TermFairMetadata from regexsolver._generated.models.term_regex import TermRegex from regexsolver._generated.models.term_request import TermRequest from regexsolver._generated.models.two_terms_request import TwoTermsRequest diff --git a/regexsolver/_generated/models/fair_response_options.py b/regexsolver/_generated/models/fair_response_options.py new file mode 100644 index 0000000..780ba17 --- /dev/null +++ b/regexsolver/_generated/models/fair_response_options.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class FairResponseOptions(BaseModel): + """ + Options controlling the FAIR output. Only applied when response format is \"fair\". + """ # noqa: E501 + deterministic: Optional[StrictBool] = Field(default=None, description="When true, the returned FAIR is guaranteed to be a deterministic automaton, suitable for consistent pagination with /generate/strings.") + __properties: ClassVar[List[str]] = ["deterministic"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of FairResponseOptions from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of FairResponseOptions from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "deterministic": obj.get("deterministic") + }) + return _obj + + diff --git a/regexsolver/_generated/models/generate_strings_request.py b/regexsolver/_generated/models/generate_strings_request.py index 83f71e6..544bc33 100644 --- a/regexsolver/_generated/models/generate_strings_request.py +++ b/regexsolver/_generated/models/generate_strings_request.py @@ -17,7 +17,7 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictBool +from pydantic import BaseModel, ConfigDict, Field from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from regexsolver._generated.models.request_options import RequestOptions @@ -27,14 +27,13 @@ class GenerateStringsRequest(BaseModel): """ - Request to generate up to 'limit' distinct strings matched by 'term', skipping the first 'offset' strings. + Request to generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. For consistent pagination, `term` should be deterministic. """ # noqa: E501 term: Term = Field(description="Source term to generate strings from.") limit: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") offset: Annotated[int, Field(strict=True, ge=0)] = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") - return_stable_term: Optional[StrictBool] = Field(default=False, description="If set to true, a stable term is returned. This term can be reused in subsequent calls to guarantee no strings are repeated. If the provided term is already stable, it will not be returned.", alias="returnStableTerm") options: Optional[RequestOptions] = None - __properties: ClassVar[List[str]] = ["term", "limit", "offset", "returnStableTerm", "options"] + __properties: ClassVar[List[str]] = ["term", "limit", "offset", "options"] model_config = ConfigDict( populate_by_name=True, @@ -96,7 +95,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, "limit": obj.get("limit"), "offset": obj.get("offset"), - "returnStableTerm": obj.get("returnStableTerm") if obj.get("returnStableTerm") is not None else False, "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None }) return _obj diff --git a/regexsolver/_generated/models/generate_strings_response.py b/regexsolver/_generated/models/generate_strings_response.py index a979899..c5d0843 100644 --- a/regexsolver/_generated/models/generate_strings_response.py +++ b/regexsolver/_generated/models/generate_strings_response.py @@ -18,20 +18,18 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar, Dict, List from regexsolver._generated.models.strings import Strings -from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self class GenerateStringsResponse(BaseModel): """ - Response containing distinct strings generated from the requested 'term'. + Response containing distinct strings generated from the requested `term`. """ # noqa: E501 type: StrictStr - term: Optional[Term] = Field(default=None, description="A stable term to use in subsequent calls to guarantee the uniqueness of generated strings. Omitted if 'returnStableTerm' was false in the request, or if the provided term was already stable.") strings: Strings = Field(description="The generated distinct strings.") - __properties: ClassVar[List[str]] = ["type", "term", "strings"] + __properties: ClassVar[List[str]] = ["type", "strings"] @field_validator('type') def type_validate_enum(cls, value): @@ -79,9 +77,6 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) - # override the default output from pydantic by calling `to_dict()` of term - if self.term: - _dict['term'] = self.term.to_dict() # override the default output from pydantic by calling `to_dict()` of strings if self.strings: _dict['strings'] = self.strings.to_dict() @@ -98,7 +93,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "type": obj.get("type"), - "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, "strings": Strings.from_dict(obj["strings"]) if obj.get("strings") is not None else None }) return _obj diff --git a/regexsolver/_generated/models/repeat_request.py b/regexsolver/_generated/models/repeat_request.py index 516f587..f7b506f 100644 --- a/regexsolver/_generated/models/repeat_request.py +++ b/regexsolver/_generated/models/repeat_request.py @@ -17,8 +17,9 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field, StrictInt +from pydantic import BaseModel, ConfigDict, Field from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated from regexsolver._generated.models.request_options import RequestOptions from regexsolver._generated.models.term import Term from typing import Optional, Set @@ -26,11 +27,11 @@ class RepeatRequest(BaseModel): """ - Request to repeat a term between 'min' and 'max' times. + Request to repeat a term between `min` and `max` times. """ # noqa: E501 term: Term = Field(description="Term to repeat.") - min: StrictInt = Field(description="Inclusive lower bound of repetitions.") - max: Optional[StrictInt] = Field(default=None, description="Inclusive upper bound. If omitted or null, the repetition is unbounded.") + min: Annotated[int, Field(strict=True, ge=0)] = Field(description="Inclusive lower bound of repetitions.") + max: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=None, description="Inclusive upper bound. If omitted or null, the repetition is unbounded.") options: Optional[RequestOptions] = None __properties: ClassVar[List[str]] = ["term", "min", "max", "options"] diff --git a/regexsolver/_generated/models/request_options.py b/regexsolver/_generated/models/request_options.py index 39661c1..fcecf97 100644 --- a/regexsolver/_generated/models/request_options.py +++ b/regexsolver/_generated/models/request_options.py @@ -26,7 +26,7 @@ class RequestOptions(BaseModel): """ - Change how the engine handle the operation. + Change how the engine handles the operation. """ # noqa: E501 schema_version: StrictInt = Field(description="Client-expected schema version.", alias="schemaVersion") response: Optional[ResponseOptions] = None diff --git a/regexsolver/_generated/models/response_options.py b/regexsolver/_generated/models/response_options.py index 24206e9..9caf806 100644 --- a/regexsolver/_generated/models/response_options.py +++ b/regexsolver/_generated/models/response_options.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator from typing import Any, ClassVar, Dict, List, Optional +from regexsolver._generated.models.fair_response_options import FairResponseOptions from typing import Optional, Set from typing_extensions import Self @@ -27,7 +28,8 @@ class ResponseOptions(BaseModel): Change how the engine returns results. """ # noqa: E501 format: Optional[StrictStr] = Field(default=None, description="Return format of the term.") - __properties: ClassVar[List[str]] = ["format"] + fair: Optional[FairResponseOptions] = Field(default=None, description="Options applied when format is \"fair\". Ignored otherwise.") + __properties: ClassVar[List[str]] = ["format", "fair"] @field_validator('format') def format_validate_enum(cls, value): @@ -78,6 +80,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of fair + if self.fair: + _dict['fair'] = self.fair.to_dict() return _dict @classmethod @@ -90,7 +95,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "format": obj.get("format") + "format": obj.get("format"), + "fair": FairResponseOptions.from_dict(obj["fair"]) if obj.get("fair") is not None else None }) return _obj diff --git a/regexsolver/_generated/models/term_fair.py b/regexsolver/_generated/models/term_fair.py index d925100..412ca39 100644 --- a/regexsolver/_generated/models/term_fair.py +++ b/regexsolver/_generated/models/term_fair.py @@ -18,7 +18,8 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional +from regexsolver._generated.models.term_fair_metadata import TermFairMetadata from typing import Optional, Set from typing_extensions import Self @@ -28,7 +29,8 @@ class TermFair(BaseModel): """ # noqa: E501 type: StrictStr value: StrictStr = Field(description="FAIR payload.") - __properties: ClassVar[List[str]] = ["type", "value"] + metadata: Optional[TermFairMetadata] = None + __properties: ClassVar[List[str]] = ["type", "value", "metadata"] @field_validator('type') def type_validate_enum(cls, value): @@ -67,8 +69,10 @@ def to_dict(self) -> Dict[str, Any]: * `None` is only added to the output dict for nullable fields that were set at model initialization. Other fields with value `None` are ignored. + * OpenAPI `readOnly` fields are excluded. """ excluded_fields: Set[str] = set([ + "metadata", ]) _dict = self.model_dump( @@ -76,6 +80,9 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # override the default output from pydantic by calling `to_dict()` of metadata + if self.metadata: + _dict['metadata'] = self.metadata.to_dict() return _dict @classmethod @@ -89,7 +96,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "type": obj.get("type"), - "value": obj.get("value") + "value": obj.get("value"), + "metadata": TermFairMetadata.from_dict(obj["metadata"]) if obj.get("metadata") is not None else None }) return _obj diff --git a/regexsolver/_generated/models/term_fair_metadata.py b/regexsolver/_generated/models/term_fair_metadata.py new file mode 100644 index 0000000..c62e27d --- /dev/null +++ b/regexsolver/_generated/models/term_fair_metadata.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" + RegexSolver + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictBool +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self + +class TermFairMetadata(BaseModel): + """ + Metadata describing properties of a FAIR automaton. + """ # noqa: E501 + deterministic: Optional[StrictBool] = Field(default=None, description="Whether this FAIR encodes a deterministic automaton. Only a deterministic FAIR guarantees consistent string ordering across paginated /generate/strings requests; call /compute/determinize first if this is false.") + __properties: ClassVar[List[str]] = ["deterministic"] + + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead + return json.dumps(self.to_dict()) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of TermFairMetadata from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of TermFairMetadata from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "deterministic": obj.get("deterministic") + }) + return _obj + + diff --git a/regexsolver/_generated/models/term_request.py b/regexsolver/_generated/models/term_request.py index 572e213..bd83624 100644 --- a/regexsolver/_generated/models/term_request.py +++ b/regexsolver/_generated/models/term_request.py @@ -26,7 +26,7 @@ class TermRequest(BaseModel): """ - Request a single term. + Request carrying a single term. """ # noqa: E501 term: Term options: Optional[RequestOptions] = None diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index f23288d..e4b77f1 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -1,7 +1,7 @@ import asyncio import logging import weakref -from typing import List, Optional +from typing import List, Optional, Union from regexsolver._generated import ( AnalyzeApi, @@ -11,6 +11,7 @@ Configuration, ErrorResponse, ExecutionOptions, + FairResponseOptions, GenerateApi, GenerateStringsRequest, MultiTermsRequest, @@ -23,17 +24,17 @@ from regexsolver.clients.rate_limiter import get_rate_limiter from regexsolver.exceptions import ( ApiError, + AutomatonTooManyStatesError, BadRequestError, ForbiddenError, InternalServerError, InvalidJsonError, - InvalidNumberOfStringsToGenerate, - AutomatonTooManyStatesError, - RegexSyntaxError, + InvalidNumberOfStringsToGenerateError, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, QuotaExceededError, + RegexSyntaxError, TimeoutExceededError, TimeoutTooLargeError, TooManyRequestsError, @@ -43,7 +44,7 @@ from regexsolver.models.cardinality import Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat -from regexsolver.models.term import Term +from regexsolver.models.term import FairTerm, Term logger = logging.getLogger(__name__) @@ -161,7 +162,7 @@ def _map_error(self, e: ApiException) -> Exception: error_msg, status_code=status_code, body=e.body ) if error_code == "InvalidNumberOfStringsToGenerate": - return InvalidNumberOfStringsToGenerate( + return InvalidNumberOfStringsToGenerateError( error_msg, status_code=status_code, body=e.body ) if error_code == "AutomatonTooManyStates": @@ -210,13 +211,33 @@ def _map_error(self, e: ApiException) -> Exception: def _build_options( self, execution_timeout: Optional[int] = None, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, ) -> RequestOptions: + if deterministic is not None and response_format is not None: + fmt = ( + ResponseFormat(response_format) + if isinstance(response_format, str) + else response_format + ) + if fmt != ResponseFormat.FAIR: + raise ValueError( + f"deterministic can only be used with response_format=ResponseFormat.FAIR, got {fmt!r}" + ) options = RequestOptions(schemaVersion=1) if execution_timeout is not None: options.execution = ExecutionOptions(timeout=execution_timeout) + response_opts = ResponseOptions() if response_format is not None: - options.response = ResponseOptions(format=response_format) + response_opts.format = str(response_format) + if deterministic is not None: + response_opts.fair = FairResponseOptions(deterministic=deterministic) + if response_format is None: + # FairResponseOptions is only applied when the response format is + # "fair", so default to it to honor the deterministic request. + response_opts.format = str(ResponseFormat.FAIR) + if response_format is not None or deterministic is not None: + options.response = response_opts return options # --- ANALYZE --- @@ -381,7 +402,7 @@ async def is_total( execution_timeout: Timeout in milliseconds for the operation. Returns: - bool: True if the term matches every possible strings. + bool: True if the term matches every possible string. """ if term._total is not None: return term._total @@ -397,6 +418,33 @@ async def is_total( term._length = Length(min=0, max=None) return response.data.value + async def is_deterministic( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Check if the term's automaton is deterministic. + Only a deterministic FAIR guarantees consistent string ordering across paginated generate_strings requests; call determinize first if this is false. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term's automaton is deterministic. + """ + if not isinstance(term, FairTerm): + return False + + if term._deterministic is not None: + return term._deterministic + request = TermRequest( + term=term.to_dto(), options=self._build_options(execution_timeout) + ) + response = await self._execute_with_retry( + self._analyze_api.deterministic, term_request=request + ) + term._deterministic = response.data.value + return response.data.value + async def get_pattern( self, term: Term, execution_timeout: Optional[int] = None ) -> str: @@ -446,7 +494,8 @@ async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> async def concat( self, *terms: Term, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Concatenates the given terms sequentially. @@ -454,6 +503,9 @@ async def concat( Args: *terms: A dynamic list of terms to concatenate in order. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -461,7 +513,9 @@ async def concat( """ request = MultiTermsRequest( terms=[t.to_dto() for t in terms], - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.concat, multi_terms_request=request @@ -471,7 +525,8 @@ async def concat( async def intersection( self, *terms: Term, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the intersection of the given terms. @@ -479,6 +534,9 @@ async def intersection( Args: *terms: A dynamic list of terms to intersect. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -486,7 +544,9 @@ async def intersection( """ request = MultiTermsRequest( terms=[t.to_dto() for t in terms], - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.intersection, multi_terms_request=request @@ -496,7 +556,8 @@ async def intersection( async def union( self, *terms: Term, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the union of the given terms. @@ -504,6 +565,9 @@ async def union( Args: *terms: A dynamic list of terms to combine. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -511,7 +575,9 @@ async def union( """ request = MultiTermsRequest( terms=[t.to_dto() for t in terms], - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.union, multi_terms_request=request @@ -522,15 +588,19 @@ async def difference( self, base_term: Term, excluded_term: Term, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: - """Computes the difference between the two provided terms. + """Computes the difference between the two given terms. Args: base_term: The base language term to subtract from. excluded_term: The term whose language should be removed from the base. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -538,7 +608,9 @@ async def difference( """ request = TwoTermsRequest( terms=[base_term.to_dto(), excluded_term.to_dto()], - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.difference, two_terms_request=request @@ -550,7 +622,8 @@ async def repeat( term: Term, min_val: int, max_val: Optional[int] = None, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Repeats a term between a minimum and maximum number of times. @@ -560,6 +633,9 @@ async def repeat( min_val: The inclusive lower bound of repetitions. max_val: The inclusive upper bound. If None, repetitions are unbounded. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -569,7 +645,9 @@ async def repeat( term=term.to_dto(), min=min_val, max=max_val, - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.repeat, repeat_request=request @@ -579,7 +657,8 @@ async def repeat( async def complement( self, term: Term, - response_format: Optional[ResponseFormat] = None, + response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the complement of the given term. @@ -587,6 +666,9 @@ async def complement( Args: term: The term to complement. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -594,13 +676,42 @@ async def complement( """ request = TermRequest( term=term.to_dto(), - options=self._build_options(execution_timeout, response_format), + options=self._build_options( + execution_timeout, response_format, deterministic + ), ) response = await self._execute_with_retry( self._compute_api.complement, term_request=request ) return Term.from_dto(response.data) + async def determinize( + self, + term: Term, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes a deterministic FAIR automaton from the given term. + + A deterministic FAIR guarantees consistent string ordering across paginated + generate_strings requests. Use this when term.is_deterministic is False or None + before calling generate_strings with an offset. + + Args: + term: The term to determinize. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A deterministic FAIR. + """ + request = TermRequest( + term=term.to_dto(), + options=self._build_options(execution_timeout), + ) + response = await self._execute_with_retry( + self._compute_api.determinize, term_request=request + ) + return Term.from_dto(response.data) + # --- GENERATE --- async def generate_strings( self, @@ -609,7 +720,7 @@ async def generate_strings( offset: int, execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `limit` distinct strings matched by 'term', skipping the first 'offset' strings. + """Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Args: term: The term to sample generated strings from. @@ -621,25 +732,14 @@ async def generate_strings( List[str]: A list of strings that match the term. """ - term_to_use = term.to_dto() - return_stable_term = False - if term._stable_term is not None: - term_to_use = term._stable_term.to_dto() - else: - return_stable_term = True - request = GenerateStringsRequest( - term=term_to_use, + term=term.to_dto(), limit=limit, offset=offset, - returnStableTerm=return_stable_term, options=self._build_options(execution_timeout), ) response = await self._execute_with_retry( self._generate_api.strings, generate_strings_request=request ) - if response.data.term is not None: - term._stable_term = Term.from_dto(response.data.term) - return response.data.strings.value diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index c0ce1e1..57e5084 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -5,6 +5,8 @@ from typing import List, Optional, Union from regexsolver.clients.asynchronous import AsyncRegexSolverClient +from regexsolver.models.cardinality import Cardinality +from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import Term @@ -85,7 +87,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() # --- ANALYZE --- - def get_cardinality(self, term: Term, execution_timeout: Optional[int] = None): + def get_cardinality( + self, term: Term, execution_timeout: Optional[int] = None + ) -> Cardinality: """Computes how many unique strings the term matches. Args: @@ -97,7 +101,9 @@ def get_cardinality(self, term: Term, execution_timeout: Optional[int] = None): """ return self._run_sync(self._aio.get_cardinality(term, execution_timeout)) - def get_length(self, term: Term, execution_timeout: Optional[int] = None): + def get_length( + self, term: Term, execution_timeout: Optional[int] = None + ) -> Length: """Computes the minimum and maximum length of strings matched by the term. Args: @@ -178,10 +184,25 @@ def is_total(self, term: Term, execution_timeout: Optional[int] = None) -> bool: execution_timeout: Timeout in milliseconds for the operation. Returns: - bool: True if the term matches every possible strings. + bool: True if the term matches every possible string. """ return self._run_sync(self._aio.is_total(term, execution_timeout)) + def is_deterministic( + self, term: Term, execution_timeout: Optional[int] = None + ) -> bool: + """Check if the term's automaton is deterministic. + Only a deterministic FAIR guarantees consistent string ordering across paginated generate_strings requests; call determinize first if this is false. + + Args: + term: The term to analyze. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + bool: True if the term's automaton is deterministic. + """ + return self._run_sync(self._aio.is_deterministic(term, execution_timeout)) + def get_pattern(self, term: Term, execution_timeout: Optional[int] = None) -> str: """Returns a regular expression pattern that represents the term. @@ -211,6 +232,7 @@ def concat( self, *terms: Term, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Concatenates the given terms sequentially. @@ -218,6 +240,9 @@ def concat( Args: *terms: A dynamic list of terms to concatenate in order. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -227,6 +252,7 @@ def concat( self._aio.concat( *terms, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) @@ -235,6 +261,7 @@ def intersection( self, *terms: Term, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the intersection of the given terms. @@ -242,6 +269,9 @@ def intersection( Args: *terms: A dynamic list of terms to intersect. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -251,6 +281,7 @@ def intersection( self._aio.intersection( *terms, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) @@ -259,6 +290,7 @@ def union( self, *terms: Term, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the union of the given terms. @@ -266,6 +298,9 @@ def union( Args: *terms: A dynamic list of terms to combine. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -275,6 +310,7 @@ def union( self._aio.union( *terms, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) @@ -284,14 +320,18 @@ def difference( base_term: Term, excluded_term: Term, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: - """Computes the difference between the two provided terms. + """Computes the difference between the two given terms. Args: base_term: The base language term to subtract from. excluded_term: The term whose language should be removed from the base. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -302,6 +342,7 @@ def difference( base_term, excluded_term, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) @@ -312,6 +353,7 @@ def repeat( min_val: int, max_val: Optional[int] = None, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Repeats a term between a minimum and maximum number of times. @@ -321,6 +363,9 @@ def repeat( min_val: The inclusive lower bound of repetitions. max_val: The inclusive upper bound. If None, repetitions are unbounded. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -332,6 +377,7 @@ def repeat( min_val, max_val, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) @@ -340,6 +386,7 @@ def complement( self, term: Term, response_format: Optional[Union[ResponseFormat, str]] = None, + deterministic: Optional[bool] = None, execution_timeout: Optional[int] = None, ) -> Term: """Computes the complement of the given term. @@ -347,6 +394,9 @@ def complement( Args: term: The term to complement. response_format: The return format of the term (any, regex or fair). + deterministic: When True, guarantees the returned FAIR encodes a deterministic + automaton. Only valid with response_format=ResponseFormat.FAIR or when + response_format is unset. Raises ValueError otherwise. execution_timeout: Timeout in milliseconds for the operation. Returns: @@ -356,10 +406,31 @@ def complement( self._aio.complement( term, response_format=response_format, + deterministic=deterministic, execution_timeout=execution_timeout, ) ) + def determinize( + self, + term: Term, + execution_timeout: Optional[int] = None, + ) -> Term: + """Computes a deterministic FAIR automaton from the given term. + + A deterministic FAIR guarantees consistent string ordering across paginated + generate_strings requests. Use this when term.is_deterministic is False or None + before calling generate_strings with an offset. + + Args: + term: The term to determinize. + execution_timeout: Timeout in milliseconds for the operation. + + Returns: + Term: A deterministic FAIR. + """ + return self._run_sync(self._aio.determinize(term, execution_timeout)) + # --- GENERATE --- def generate_strings( self, @@ -368,7 +439,7 @@ def generate_strings( offset: int, execution_timeout: Optional[int] = None, ) -> List[str]: - """Generates up to `limit` distinct strings matched by 'term', skipping the first 'offset' strings. + """Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Args: term: The term to sample generated strings from. diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index f3de5cd..0f8e633 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -56,7 +56,7 @@ class TimeoutExceededError(BadRequestError): pass -class InvalidNumberOfStringsToGenerate(BadRequestError): +class InvalidNumberOfStringsToGenerateError(BadRequestError): """Raised when the requested number of strings to generate is below the minimum or exceeds the maximum allowed.""" pass diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index 55c9436..c7868eb 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -25,7 +25,6 @@ def __init__(self, value: str): self._total: Optional[bool] = None self._pattern: Optional[str] = None self._dot: Optional[str] = None - self._stable_term: Optional["Term"] = None self._compiled_regex: Optional[Pattern] = None @@ -71,7 +70,7 @@ def _set_properties_mixin(self, properties_mixin: TermPropertiesMixin): if total is not None: self._total = total - def is_match(self, string: str) -> bool: + def matches(self, string: str) -> bool: """Client-side matching implementation.""" pattern = self.get_pattern() if pattern is None: @@ -110,19 +109,17 @@ def from_dto(cls, dto: GeneratedTerm) -> "Term": actual_instance = dto.actual_instance if actual_instance is None: raise RuntimeError("Invalid Term DTO provided.") - if actual_instance.type == "regex": - return cls.regex(actual_instance.value) - else: - return cls.fair(actual_instance.value) + if isinstance(actual_instance, TermFair): + deterministic = ( + actual_instance.metadata.deterministic + if actual_instance.metadata is not None + else None + ) + return FairTerm(actual_instance.value, deterministic=deterministic) + return cls.regex(actual_instance.value) # --- Shared Getters/Setters --- - def get_cached_stable_term(self) -> Optional["Term"]: - return self._stable_term - - def set_cached_stable_term(self, stable_term: Optional["Term"]): - self._stable_term = stable_term - def __eq__(self, other: Any) -> bool: if self is other: return True @@ -145,8 +142,7 @@ def get_pattern(self) -> Optional[str]: return self.get_value() def get_fair(self) -> Optional[str]: - stable = self.get_cached_stable_term() - return stable.get_fair() if stable else None + return None def to_dto(self) -> GeneratedTerm: return GeneratedTerm(TermRegex(type="regex", value=self.get_value())) @@ -156,6 +152,15 @@ def serialize(self) -> str: class FairTerm(Term): + def __init__(self, value: str, deterministic: Optional[bool] = None): + super().__init__(value) + self._deterministic = deterministic + + @property + def is_deterministic(self) -> Optional[bool]: + """Whether this FAIR encodes a deterministic automaton, or None if unknown.""" + return self._deterministic + def get_pattern(self) -> Optional[str]: return self._pattern diff --git a/tests/test_async_client.py b/tests/test_async_client.py index e008947..ff5b666 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -10,7 +10,7 @@ Infinite, Integer, InvalidJsonError, - InvalidNumberOfStringsToGenerate, + InvalidNumberOfStringsToGenerateError, InvalidTokenError, MissingOrMalformedTokenError, NotFoundError, @@ -21,7 +21,7 @@ TooManyTermsError, UnauthorizedError, ) -from regexsolver.generated import ApiException +from regexsolver._generated import ApiException @pytest.fixture @@ -176,7 +176,7 @@ async def test_error_handling_invalid_number_of_strings_to_generate(async_client error_400 = ApiException(status=400) error_400.body = '{"success": false, "error": "Too many strings", "errorCode": "InvalidNumberOfStringsToGenerate"}' async_client._generate_api.strings.side_effect = error_400 - with pytest.raises(InvalidNumberOfStringsToGenerate): + with pytest.raises(InvalidNumberOfStringsToGenerateError): await async_client.generate_strings(Term.regex("abc"), 100, 0) diff --git a/tests/test_models.py b/tests/test_models.py index 4884872..a7d66ec 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,15 +1,16 @@ import pytest -from regexsolver.generated.models import Cardinality as GeneratedCardinality -from regexsolver.generated.models import ( +from regexsolver._generated.models import Cardinality as GeneratedCardinality +from regexsolver._generated.models import ( CardinalityBigInteger, CardinalityInfinite, CardinalityInteger, TermFair, TermRegex, ) -from regexsolver.generated.models import Length as GeneratedLength -from regexsolver.generated.models import Term as GeneratedTerm +from regexsolver._generated.models import Length as GeneratedLength +from regexsolver._generated.models import Term as GeneratedTerm +from regexsolver._generated.models.term_fair_metadata import TermFairMetadata from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.term import FairTerm, RegexTerm, Term @@ -173,14 +174,130 @@ def test_term_serialize_deserialize(): assert Term.deserialize("unknown=value") is None -def test_term_is_match(): +def test_term_matches(): term = Term.regex("a.b") - assert term.is_match("axb") is True - assert term.is_match("a\nb") is True # DOTALL - assert term.is_match("ab") is False - assert term.is_match("axxb") is False # anchored (fullmatch) + assert term.matches("axb") is True + assert term.matches("a\nb") is True # DOTALL + assert term.matches("ab") is False + assert term.matches("axxb") is False # anchored (fullmatch) fair_term = Term.fair("payload") # Matches the new Java-aligned behavior of throwing an exception with pytest.raises(RuntimeError, match="not defined yet"): - fair_term.is_match("abc") + fair_term.matches("abc") + + +# --- FairTerm.is_deterministic --- + + +def test_fair_term_is_deterministic_unknown_by_default(): + term = Term.fair("payload") + assert isinstance(term, FairTerm) + assert term.is_deterministic is None + + +def test_fair_term_public_factory_has_no_deterministic_param(): + with pytest.raises(TypeError): + Term.fair("payload", deterministic=True) # type: ignore[call-arg] + + +def test_from_dto_fair_with_deterministic_true(): + dto = GeneratedTerm( + TermFair( + type="fair", value="payload", metadata=TermFairMetadata(deterministic=True) + ) + ) + term = Term.from_dto(dto) + assert isinstance(term, FairTerm) + assert term.is_deterministic is True + + +def test_from_dto_fair_with_deterministic_false(): + dto = GeneratedTerm( + TermFair( + type="fair", value="payload", metadata=TermFairMetadata(deterministic=False) + ) + ) + term = Term.from_dto(dto) + assert isinstance(term, FairTerm) + assert term.is_deterministic is False + + +def test_from_dto_fair_without_metadata(): + dto = GeneratedTerm(TermFair(type="fair", value="payload")) + term = Term.from_dto(dto) + assert isinstance(term, FairTerm) + assert term.is_deterministic is None + + +# --- metadata is never sent to the server --- + + +def test_fair_term_to_dto_excludes_metadata(): + # FairTerm.to_dto() always builds a fresh TermFair without metadata — + # metadata is never round-tripped back to the server. + term = Term.from_dto( + GeneratedTerm( + TermFair( + type="fair", + value="payload", + metadata=TermFairMetadata(deterministic=True), + ) + ) + ) + instance = term.to_dto().actual_instance + assert instance is not None + dto_dict = instance.to_dict() + assert "metadata" not in dto_dict + assert dto_dict == {"type": "fair", "value": "payload"} + + +# --- deterministic + response_format validation --- + + +def _make_client(): + from regexsolver.clients.asynchronous import AsyncRegexSolverClient + + return AsyncRegexSolverClient.__new__(AsyncRegexSolverClient) + + +def test_build_options_deterministic_with_fair_format_ok(): + from regexsolver.models.response_format import ResponseFormat + + client = _make_client() + opts = client._build_options( + response_format=ResponseFormat.FAIR, deterministic=True + ) + assert opts.response is not None + assert opts.response.fair is not None + assert opts.response.fair.deterministic is True + + +def test_build_options_deterministic_without_format_ok(): + client = _make_client() + opts = client._build_options(deterministic=True) + assert opts.response is not None + assert opts.response.fair is not None + assert opts.response.fair.deterministic is True + + +def test_build_options_deterministic_with_regex_format_raises(): + from regexsolver.models.response_format import ResponseFormat + + client = _make_client() + with pytest.raises(ValueError, match="deterministic"): + client._build_options(response_format=ResponseFormat.REGEX, deterministic=True) + + +def test_build_options_deterministic_with_any_format_raises(): + from regexsolver.models.response_format import ResponseFormat + + client = _make_client() + with pytest.raises(ValueError, match="deterministic"): + client._build_options(response_format=ResponseFormat.ANY, deterministic=True) + + +def test_build_options_deterministic_with_string_regex_raises(): + client = _make_client() + with pytest.raises(ValueError, match="deterministic"): + client._build_options(response_format="regex", deterministic=True) diff --git a/tests/test_sync_client.py b/tests/test_sync_client.py index ce8cd25..937d4ce 100644 --- a/tests/test_sync_client.py +++ b/tests/test_sync_client.py @@ -38,7 +38,7 @@ def test_sync_client_union(): assert result == mock_result_term client._aio.union.assert_called_once_with( - term1, term2, response_format=None, execution_timeout=None + term1, term2, response_format=None, deterministic=None, execution_timeout=None ) @@ -52,7 +52,7 @@ def test_sync_client_complement(): assert result == mock_result_term client._aio.complement.assert_called_once_with( - term, response_format=None, execution_timeout=None + term, response_format=None, deterministic=None, execution_timeout=None ) From 6ec42eb490a660da171df6b7af96dc3490c322fa Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:10:35 +0200 Subject: [PATCH 43/47] Add new error --- regexsolver/__init__.py | 2 ++ regexsolver/_generated/models/error_response400.py | 4 ++-- regexsolver/clients/asynchronous.py | 3 +++ regexsolver/exceptions.py | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index b91f1e4..ca7dd15 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -4,6 +4,7 @@ ApiError, AutomatonTooManyStatesError, BadRequestError, + FairSyntaxError, ForbiddenError, InternalServerError, InvalidJsonError, @@ -32,6 +33,7 @@ "ApiError", "AutomatonTooManyStatesError", "BadRequestError", + "FairSyntaxError", "ForbiddenError", "InternalServerError", "InvalidJsonError", diff --git a/regexsolver/_generated/models/error_response400.py b/regexsolver/_generated/models/error_response400.py index 9b1c95d..8e7e1c7 100644 --- a/regexsolver/_generated/models/error_response400.py +++ b/regexsolver/_generated/models/error_response400.py @@ -37,8 +37,8 @@ def error_code_validate_enum(cls, value): if value is None: return value - if value not in set(['InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError']): - raise ValueError("must be one of enum values ('InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError')") + if value not in set(['InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError']): + raise ValueError("must be one of enum values ('InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError')") return value model_config = ConfigDict( diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index e4b77f1..1871be1 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -26,6 +26,7 @@ ApiError, AutomatonTooManyStatesError, BadRequestError, + FairSyntaxError, ForbiddenError, InternalServerError, InvalidJsonError, @@ -171,6 +172,8 @@ def _map_error(self, e: ApiException) -> Exception: ) if error_code == "RegexSyntaxError": return RegexSyntaxError(error_msg, status_code=status_code, body=e.body) + if error_code == "FairSyntaxError": + return FairSyntaxError(error_msg, status_code=status_code, body=e.body) return BadRequestError(error_msg, status_code=status_code, body=e.body) elif status_code == 401: diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 0f8e633..6fda1d1 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -74,6 +74,12 @@ class RegexSyntaxError(BadRequestError): pass +class FairSyntaxError(BadRequestError): + """Raised when the provided FAIR value is malformed or cannot be decoded.""" + + pass + + class UnauthorizedError(ApiError): """Raised when the API returns a 401 Unauthorized error.""" From c32f8e347078acb9f759644956d70cab44aff62b Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:50:23 +0200 Subject: [PATCH 44/47] Fix some issues --- regexsolver/__init__.py | 6 +- .../_generated/models/error_response400.py | 4 +- regexsolver/clients/asynchronous.py | 76 ++++++++++++++----- regexsolver/exceptions.py | 6 ++ regexsolver/models/term.py | 9 +++ 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index ca7dd15..0b372bb 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -17,6 +17,7 @@ RegexSyntaxError, TimeoutExceededError, TimeoutTooLargeError, + TooFewTermsError, TooManyRequestsError, TooManyTermsError, UnauthorizedError, @@ -24,12 +25,14 @@ from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat -from regexsolver.models.term import Term +from regexsolver.models.term import FairTerm, RegexTerm, Term __all__ = [ "AsyncRegexSolverClient", "RegexSolverClient", "Term", + "FairTerm", + "RegexTerm", "ApiError", "AutomatonTooManyStatesError", "BadRequestError", @@ -47,6 +50,7 @@ "TimeoutTooLargeError", "TooManyRequestsError", "InvalidNumberOfStringsToGenerateError", + "TooFewTermsError", "TooManyTermsError", "UnauthorizedError", "BigInteger", diff --git a/regexsolver/_generated/models/error_response400.py b/regexsolver/_generated/models/error_response400.py index 8e7e1c7..7cd4626 100644 --- a/regexsolver/_generated/models/error_response400.py +++ b/regexsolver/_generated/models/error_response400.py @@ -37,8 +37,8 @@ def error_code_validate_enum(cls, value): if value is None: return value - if value not in set(['InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError']): - raise ValueError("must be one of enum values ('InvalidJson', 'TooManyTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError')") + if value not in set(['InvalidJson', 'TooManyTerms', 'TooFewTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError']): + raise ValueError("must be one of enum values ('InvalidJson', 'TooManyTerms', 'TooFewTerms', 'TimeoutTooLarge', 'TimeoutExceeded', 'InvalidNumberOfStringsToGenerate', 'AutomatonTooManyStates', 'RegexSyntaxError', 'FairSyntaxError')") return value model_config = ConfigDict( diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 1871be1..9fcb9ce 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -3,6 +3,8 @@ import weakref from typing import List, Optional, Union +from pydantic import ValidationError + from regexsolver._generated import ( AnalyzeApi, ApiClient, @@ -35,9 +37,11 @@ MissingOrMalformedTokenError, NotFoundError, QuotaExceededError, + RegexSolverError, RegexSyntaxError, TimeoutExceededError, TimeoutTooLargeError, + TooFewTermsError, TooManyRequestsError, TooManyTermsError, UnauthorizedError, @@ -50,6 +54,36 @@ logger = logging.getLogger(__name__) +def _build_request(model, **kwargs): + """Build a generated request model, keeping pydantic out of the public surface. + + The generated models carry the constraints declared in openapi.yaml (`terms` + minItems, `limit` range), so an invalid call is rejected before it is sent -- + which is good, it saves a round trip. But the raw `pydantic.ValidationError` + is not a `RegexSolverError`, so callers writing `except RegexSolverError` + would miss it. Translate it into the same error the API would have returned. + """ + try: + return model(**kwargs) + except ValidationError as e: + raise _map_validation_error(e) from e + + +def _map_validation_error(e: ValidationError) -> RegexSolverError: + errors = e.errors() + fields = {str(err["loc"][0]) for err in errors if err.get("loc")} + message = "; ".join( + f"{'.'.join(str(part) for part in err.get('loc', ()))}: {err['msg']}" + for err in errors + ) + + if "terms" in fields: + return TooFewTermsError(message, status_code=400) + if fields & {"limit", "offset"}: + return InvalidNumberOfStringsToGenerateError(message, status_code=400) + return BadRequestError(message, status_code=400) + + class AsyncRegexSolverClient: """The Asynchronous Client for RegexSolver. @@ -154,6 +188,10 @@ def _map_error(self, e: ApiException) -> Exception: return TooManyTermsError( error_msg, status_code=status_code, body=e.body ) + if error_code == "TooFewTerms": + return TooFewTermsError( + error_msg, status_code=status_code, body=e.body + ) if error_code == "TimeoutTooLarge": return TimeoutTooLargeError( error_msg, status_code=status_code, body=e.body @@ -259,7 +297,7 @@ async def get_cardinality( if term._cardinality is not None: return term._cardinality - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -285,7 +323,7 @@ async def get_length( if term._length is not None: return term._length - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -309,7 +347,7 @@ async def equivalent( Returns: bool: True if they are entirely equivalent, False otherwise. """ - request = TwoTermsRequest( + request = _build_request(TwoTermsRequest, terms=[term1.to_dto(), term2.to_dto()], options=self._build_options(execution_timeout), ) @@ -334,7 +372,7 @@ async def subset( Returns: bool: True if every string matched by `term_subset` is also matched by `term_superset`. """ - request = TwoTermsRequest( + request = _build_request(TwoTermsRequest, terms=[term_subset.to_dto(), term_superset.to_dto()], options=self._build_options(execution_timeout), ) @@ -357,7 +395,7 @@ async def is_empty( """ if term._empty is not None: return term._empty - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -383,7 +421,7 @@ async def is_empty_string( """ if term._empty_string is not None: return term._empty_string - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -409,7 +447,7 @@ async def is_total( """ if term._total is not None: return term._total - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -439,7 +477,7 @@ async def is_deterministic( if term._deterministic is not None: return term._deterministic - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -460,10 +498,10 @@ async def get_pattern( Returns: str: A valid regular expression string representing the language. """ - pattern = term.get_pattern() + pattern = term._pattern if pattern is not None: return pattern - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -484,7 +522,7 @@ async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> """ if term._dot is not None: return term._dot - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -514,7 +552,7 @@ async def concat( Returns: Term: A newly computed concatenated term. """ - request = MultiTermsRequest( + request = _build_request(MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -545,7 +583,7 @@ async def intersection( Returns: Term: A term representing only strings matched by ALL provided terms. """ - request = MultiTermsRequest( + request = _build_request(MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -576,7 +614,7 @@ async def union( Returns: Term: A term representing strings matched by ANY of the provided terms. """ - request = MultiTermsRequest( + request = _build_request(MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -609,7 +647,7 @@ async def difference( Returns: Term: A computed difference term. """ - request = TwoTermsRequest( + request = _build_request(TwoTermsRequest, terms=[base_term.to_dto(), excluded_term.to_dto()], options=self._build_options( execution_timeout, response_format, deterministic @@ -644,7 +682,7 @@ async def repeat( Returns: Term: A computed repeated term. """ - request = RepeatRequest( + request = _build_request(RepeatRequest, term=term.to_dto(), min=min_val, max=max_val, @@ -677,7 +715,7 @@ async def complement( Returns: Term: The complemented term. """ - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options( execution_timeout, response_format, deterministic @@ -706,7 +744,7 @@ async def determinize( Returns: Term: A deterministic FAIR. """ - request = TermRequest( + request = _build_request(TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout), ) @@ -735,7 +773,7 @@ async def generate_strings( List[str]: A list of strings that match the term. """ - request = GenerateStringsRequest( + request = _build_request(GenerateStringsRequest, term=term.to_dto(), limit=limit, offset=offset, diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 6fda1d1..4e8b99c 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -44,6 +44,12 @@ class TooManyTermsError(BadRequestError): pass +class TooFewTermsError(BadRequestError): + """Raised when fewer terms are provided than the operation requires.""" + + pass + + class TimeoutTooLargeError(BadRequestError): """Raised when the requested `execution_timeout` exceeds the maximum allowed for your current plan.""" diff --git a/regexsolver/models/term.py b/regexsolver/models/term.py index c7868eb..5fbfcf2 100644 --- a/regexsolver/models/term.py +++ b/regexsolver/models/term.py @@ -11,6 +11,10 @@ from regexsolver.models.term_properties_mixin import TermPropertiesMixin +EMPTY_LANGUAGE_PATTERN = "[]" +"""How the engine renders a language that matches no string at all.""" + + class Term(ABC): """Represents a mathematical term (Regex or FAIR) on which operations can be performed.""" @@ -78,6 +82,11 @@ def matches(self, string: str) -> bool: "The regex pattern of this term is not defined yet, call get_pattern() on the client to set it." ) + # The engine renders the empty language as "[]". By definition it matches + # nothing, and `re` rejects the pattern outright. + if pattern == EMPTY_LANGUAGE_PATTERN: + return False + if self._compiled_regex is None: try: self._compiled_regex = re.compile(rf"\A(?:{pattern})\Z", re.DOTALL) From 4af4ce30cc6fd12f1ce55aba3c746ccd2778fcb9 Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:15:14 +0200 Subject: [PATCH 45/47] Align repo with the Java and JS SDKs, and make the lint gate pass The CI lint job could not pass: flake8 reported ~700 violations (mostly from the generated package) and mypy 2 errors. Generated code is now excluded from both, and the real issues in hand-written code are fixed: 18 malformed continuation lines, 6 overlong docstrings, and a cast(Any, ...) in cardinality.py that was the intent behind the unused cast import. Packaging had three sources of version truth: pyproject.toml, setup.py, and the generator-emitted requirements files, which nothing consumes. pyproject is now the single manifest, matching the Java pom and the JS package.json. generate-api.sh appended pytest-asyncio to test-requirements.txt on every run; both files are now generator-ignored. - publish.yml runs the test suite before building, 2-space indentation like the other workflows, and spells PyPI correctly - README: the API overview claimed every method accepts response_format; analyze operations and determinize() take execution_timeout only. Added the async notes after each table, as in the Java README, and fixed a typo - pinned openapi-generator 7.21.0, as in the Java and JS repos - .gitignore rewritten in the shared layout, with .env and IDE entries Co-Authored-By: Claude Opus 5 --- .flake8 | 10 ++ .github/workflows/publish.yml | 116 ++++++++++++-------- .gitignore | 73 +++++------- .openapi-generator-ignore | 6 +- README.md | 11 +- generate-api.sh | 3 +- openapitools.json | 2 +- pyproject.toml | 12 ++ regexsolver/clients/asynchronous.py | 57 ++++++---- regexsolver/clients/synchronous.py | 3 +- regexsolver/exceptions.py | 3 +- regexsolver/models/cardinality.py | 4 +- regexsolver/models/term_properties_mixin.py | 9 +- requirements.txt | 5 - setup.py | 51 --------- test-requirements.txt | 7 -- 16 files changed, 185 insertions(+), 187 deletions(-) create mode 100644 .flake8 delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 test-requirements.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e1a9fdf --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 120 +# E203 (whitespace before ':') conflicts with black's slice formatting. +extend-ignore = E203 +extend-exclude = + regexsolver/_generated, + .venv, + venv, + build, + dist diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bc332dd..3407c30 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,49 +1,73 @@ -name: Publish to PyPi +name: Publish to PyPI on: - push: - tags: - - 'v*' - + push: + tags: + - "v*" + jobs: - build: - name: Build distribution - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Install pypa/build - run: >- - python3 -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: python3 -m build - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - publish-to-pypi: - name: Publish to PyPI - needs: - - build - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/regexsolver - permissions: - id-token: write - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + test: + name: Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[test]" + + - name: Run tests with pytest + run: pytest + + build: + name: Build distribution + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install pypa/build + run: python3 -m pip install build --user + + - name: Build a binary wheel and a source tarball + run: python3 -m build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/regexsolver + permissions: + id-token: write + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 65b06b9..34fe970 100644 --- a/.gitignore +++ b/.gitignore @@ -1,66 +1,49 @@ +# Build output +build/ +dist/ +sdist/ +*.egg-info/ +*.egg +.eggs/ +develop-eggs/ +.installed.cfg + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class - -# C extensions *.so -# Distribution / packaging +# Virtual environments .Python env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ +venv/ +.venv/ lib/ lib64/ parts/ -sdist/ var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +.python-version -# Unit test / coverage reports -htmlcov/ +# Test / coverage / type checking +.pytest_cache/ +.mypy_cache/ .tox/ +.cache +htmlcov/ .coverage .coverage.* -.cache -nosetests.xml coverage.xml -*,cover +nosetests.xml .hypothesis/ -venv/ -.venv/ -.python-version -.pytest_cache - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -# Sphinx documentation -docs/_build/ +# Environment +.env -# PyBuilder -target/ +# IDE +.idea/ +.vscode/ +*.iml -# Ipython Notebook -.ipynb_checkpoints +# OS +.DS_Store diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index c968472..6289d05 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -1,6 +1,9 @@ setup.py setup.cfg tox.ini +.gitignore +requirements.txt +test-requirements.txt git_push.sh .travis.yml .gitlab-ci.yml @@ -10,8 +13,7 @@ docs/ test/ README.md - regexsolver/__init__.py -regexsolver/client.py +regexsolver/clients/* regexsolver/exceptions.py regexsolver/models/* diff --git a/README.md b/README.md index 2ec73fd..1fcc081 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ The API can handle terms in two formats: - `regex`: a regular expression pattern - `fair`: FAIR (Fast Automaton Internal Representation), a stable, signed format used internally by the engine -By default, the engine returns whatever the operation produces, with no extra convertion. Override with `response_format`: +By default, the engine returns whatever the operation produces, with no extra conversion. Override with `response_format`, accepted by the operations that return a term: + ```python from regexsolver import ResponseFormat @@ -109,7 +110,7 @@ Timeout is best effort. The exact time is not guaranteed. ## API Overview -`RegexSolverClient` and `AsyncRegexSolverClient` exposes the following methods. All methods accept optional keyword arguments `response_format` and `execution_timeout`. +`RegexSolverClient` and `AsyncRegexSolverClient` expose the following methods. Every method accepts optional keyword arguments: operations that return a term take `response_format`, `deterministic` and `execution_timeout`, while analyze operations and `determinize()` take `execution_timeout` only — the response format is not theirs to choose. ### Analyze @@ -126,6 +127,8 @@ Timeout is best effort. The exact time is not guaranteed. | `client.is_deterministic(term, **kwargs)` | `bool` | `True` if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated `generate_strings()` calls; call `determinize()` first if this is `False`. | | `client.subset(term1, term2, **kwargs)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | +*Note: For `AsyncRegexSolverClient`, these methods are coroutines and must be awaited.* + ### Compute | Method | Return | Description | @@ -138,12 +141,16 @@ Timeout is best effort. The exact time is not guaranteed. | `client.repeat(term, min, max, **kwargs)` | `Term` | Computes the repetition of the term between `min` and `max` times. | | `client.union(term1, term2, ..., **kwargs)` | `Term` | Computes the union of the given terms. | +*Note: For `AsyncRegexSolverClient`, these methods are coroutines and must be awaited.* + ### Generate | Method | Return | Description | | -------- | ------- | ------- | | `client.generate_strings(term, limit, offset, **kwargs)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. | +*Note: For `AsyncRegexSolverClient`, this method is a coroutine and must be awaited.* + ## 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 68bf3ee..e3831c0 100755 --- a/generate-api.sh +++ b/generate-api.sh @@ -11,5 +11,4 @@ openapi-generator-cli generate \ -o "$OUT_DIR" \ --additional-properties=packageName="$PACKAGE_NAME",library=asyncio - -echo "pytest-asyncio >= 1.3.0" >> test-requirements.txt +echo "API Generation Complete." diff --git a/openapitools.json b/openapitools.json index c121433..91d9c43 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.20.0" + "version": "7.21.0" } } diff --git a/pyproject.toml b/pyproject.toml index 1777dba..2e70a26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,18 @@ Issues = "https://github.com/RegexSolver/regexsolver-python/issues" Documentation = "https://docs.regexsolver.com/" "Source Code" = "https://github.com/RegexSolver/regexsolver-python" +[tool.setuptools.packages.find] +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. +[[tool.mypy.overrides]] +module = "regexsolver._generated.*" +follow_imports = "silent" + [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 9fcb9ce..37a374f 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -297,7 +297,8 @@ async def get_cardinality( if term._cardinality is not None: return term._cardinality - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -323,7 +324,8 @@ async def get_length( if term._length is not None: return term._length - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -347,7 +349,8 @@ async def equivalent( Returns: bool: True if they are entirely equivalent, False otherwise. """ - request = _build_request(TwoTermsRequest, + request = _build_request( + TwoTermsRequest, terms=[term1.to_dto(), term2.to_dto()], options=self._build_options(execution_timeout), ) @@ -372,7 +375,8 @@ async def subset( Returns: bool: True if every string matched by `term_subset` is also matched by `term_superset`. """ - request = _build_request(TwoTermsRequest, + request = _build_request( + TwoTermsRequest, terms=[term_subset.to_dto(), term_superset.to_dto()], options=self._build_options(execution_timeout), ) @@ -395,7 +399,8 @@ async def is_empty( """ if term._empty is not None: return term._empty - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -421,7 +426,8 @@ async def is_empty_string( """ if term._empty_string is not None: return term._empty_string - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -447,7 +453,8 @@ async def is_total( """ if term._total is not None: return term._total - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -463,7 +470,8 @@ async def is_deterministic( self, term: Term, execution_timeout: Optional[int] = None ) -> bool: """Check if the term's automaton is deterministic. - Only a deterministic FAIR guarantees consistent string ordering across paginated generate_strings requests; call determinize first if this is false. + Only a deterministic FAIR guarantees consistent string ordering across + paginated generate_strings requests; call determinize first if this is false. Args: term: The term to analyze. @@ -477,7 +485,8 @@ async def is_deterministic( if term._deterministic is not None: return term._deterministic - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -501,7 +510,8 @@ async def get_pattern( pattern = term._pattern if pattern is not None: return pattern - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -522,7 +532,8 @@ async def get_dot(self, term: Term, execution_timeout: Optional[int] = None) -> """ if term._dot is not None: return term._dot - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout) ) response = await self._execute_with_retry( @@ -552,7 +563,8 @@ async def concat( Returns: Term: A newly computed concatenated term. """ - request = _build_request(MultiTermsRequest, + request = _build_request( + MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -583,7 +595,8 @@ async def intersection( Returns: Term: A term representing only strings matched by ALL provided terms. """ - request = _build_request(MultiTermsRequest, + request = _build_request( + MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -614,7 +627,8 @@ async def union( Returns: Term: A term representing strings matched by ANY of the provided terms. """ - request = _build_request(MultiTermsRequest, + request = _build_request( + MultiTermsRequest, terms=[t.to_dto() for t in terms], options=self._build_options( execution_timeout, response_format, deterministic @@ -647,7 +661,8 @@ async def difference( Returns: Term: A computed difference term. """ - request = _build_request(TwoTermsRequest, + request = _build_request( + TwoTermsRequest, terms=[base_term.to_dto(), excluded_term.to_dto()], options=self._build_options( execution_timeout, response_format, deterministic @@ -682,7 +697,8 @@ async def repeat( Returns: Term: A computed repeated term. """ - request = _build_request(RepeatRequest, + request = _build_request( + RepeatRequest, term=term.to_dto(), min=min_val, max=max_val, @@ -715,7 +731,8 @@ async def complement( Returns: Term: The complemented term. """ - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options( execution_timeout, response_format, deterministic @@ -744,7 +761,8 @@ async def determinize( Returns: Term: A deterministic FAIR. """ - request = _build_request(TermRequest, + request = _build_request( + TermRequest, term=term.to_dto(), options=self._build_options(execution_timeout), ) @@ -773,7 +791,8 @@ async def generate_strings( List[str]: A list of strings that match the term. """ - request = _build_request(GenerateStringsRequest, + request = _build_request( + GenerateStringsRequest, term=term.to_dto(), limit=limit, offset=offset, diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index 57e5084..be2a346 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -192,7 +192,8 @@ def is_deterministic( self, term: Term, execution_timeout: Optional[int] = None ) -> bool: """Check if the term's automaton is deterministic. - Only a deterministic FAIR guarantees consistent string ordering across paginated generate_strings requests; call determinize first if this is false. + Only a deterministic FAIR guarantees consistent string ordering across + paginated generate_strings requests; call determinize first if this is false. Args: term: The term to analyze. diff --git a/regexsolver/exceptions.py b/regexsolver/exceptions.py index 4e8b99c..876fc0d 100644 --- a/regexsolver/exceptions.py +++ b/regexsolver/exceptions.py @@ -57,7 +57,8 @@ class TimeoutTooLargeError(BadRequestError): class TimeoutExceededError(BadRequestError): - """Raised when the execution of the request exceeds the provided `execution_timeout` or the maximum allowed for your current plan.""" + """Raised when the execution of the request exceeds the provided + `execution_timeout` or the maximum allowed for your current plan.""" pass diff --git a/regexsolver/models/cardinality.py b/regexsolver/models/cardinality.py index 2eae12b..06f0104 100644 --- a/regexsolver/models/cardinality.py +++ b/regexsolver/models/cardinality.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional, cast +from typing import Any, Optional, cast from regexsolver._generated.models import Cardinality as GeneratedCardinality from regexsolver.models.term_properties_mixin import TermPropertiesMixin @@ -21,7 +21,7 @@ def from_dto(cls, dto: GeneratedCardinality) -> "Cardinality": Raises: ValueError: If the DTO contains an unknown cardinality type. """ - actual_model = getattr(dto, "actual_instance", dto) + actual_model = cast(Any, getattr(dto, "actual_instance", dto)) c_type = actual_model.type if c_type == "infinite": diff --git a/regexsolver/models/term_properties_mixin.py b/regexsolver/models/term_properties_mixin.py index 4752546..4ec117a 100644 --- a/regexsolver/models/term_properties_mixin.py +++ b/regexsolver/models/term_properties_mixin.py @@ -11,7 +11,8 @@ def is_empty(self) -> Optional[bool]: """Infers whether the term matches no strings at all. Returns: - Optional[bool]: True if it definitely matches no strings, False if it matches at least one, or None if it cannot be inferred. + Optional[bool]: True if it definitely matches no strings, False if it + matches at least one, or None if it cannot be inferred. """ return None @@ -19,7 +20,8 @@ def is_empty_string(self) -> Optional[bool]: """Infers whether the term matches strictly the empty string (""). Returns: - Optional[bool]: True if it definitely matches only the empty string, False if it matches other strings, or None if it cannot be inferred. + Optional[bool]: True if it definitely matches only the empty string, + False if it matches other strings, or None if it cannot be inferred. """ return None @@ -27,6 +29,7 @@ def is_total(self) -> Optional[bool]: """Infers whether the term matches all possible strings. Returns: - Optional[bool]: True if it definitely matches all strings, False if it misses at least one string, or None if it cannot be inferred. + Optional[bool]: True if it definitely matches all strings, False if it + misses at least one string, or None if it cannot be inferred. """ return None diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8c5c440..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -python_dateutil >= 2.8.2 -aiohttp >= 3.8.4 -aiohttp-retry >= 2.8.3 -pydantic >= 2 -typing-extensions >= 4.7.1 diff --git a/setup.py b/setup.py deleted file mode 100644 index fa5df4d..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="regexsolver", - version="1.1.0", - description="RegexSolver is a powerful toolkit for building, combining, and analyzing regular expressions.", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - author="RegexSolver", - author_email="contact@regexsolver.com", - url="https://github.com/RegexSolver/regexsolver-python", - license="MIT", - keywords="regex regexp pattern intersection union difference concat equivalence subset nfa dfa", - packages=find_packages(exclude=["tests", "tests.*"]), - install_requires=[ - "aiohttp>=3.8.4", - "aiohttp-retry>=2.8.3", - "python-dateutil>=2.8.2", - "pydantic>=2.0.0", - "typing-extensions>=4.7.1", - ], - extras_require={ - "test": [ - "pytest>=7.2.1", - "pytest-cov>=2.8.1", - "pytest-asyncio>=1.3.0", - "tox>=3.9.0", - "flake8>=4.0.0", - "mypy>=1.5", - "types-python-dateutil>=2.8.19.14", - ] - }, - python_requires=">=3.9", - project_urls={ - "Homepage": "https://regexsolver.com/", - "Issues": "https://github.com/RegexSolver/regexsolver-python/issues", - "Documentation": "https://docs.regexsolver.com/", - "Source Code": "https://github.com/RegexSolver/regexsolver-python", - }, - classifiers=[ - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: Libraries :: Python Modules", - ], -) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 3104aef..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -pytest >= 7.2.1 -pytest-cov >= 2.8.1 -tox >= 3.9.0 -flake8 >= 4.0.0 -types-python-dateutil >= 2.8.19.14 -mypy >= 1.5 -pytest-asyncio >= 1.3.0 From d5622a6ebf807eb487017b6db78dd5962e95eb8a Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Tue, 4 Aug 2026 20:54:06 +0200 Subject: [PATCH 46/47] Update library --- .openapi-generator/FILES | 8 +- .openapi-generator/VERSION | 2 +- README.md | 10 +- regexsolver/__init__.py | 5 + regexsolver/_generated/__init__.py | 12 +- regexsolver/_generated/api/__init__.py | 1 + regexsolver/_generated/api/account_api.py | 293 ++++++++++++++++++ regexsolver/_generated/api/analyze_api.py | 2 +- regexsolver/_generated/api/compute_api.py | 2 +- regexsolver/_generated/api/generate_api.py | 8 +- regexsolver/_generated/api_client.py | 7 +- regexsolver/_generated/configuration.py | 49 ++- regexsolver/_generated/exceptions.py | 2 +- regexsolver/_generated/models/__init__.py | 6 +- .../_generated/models/account_limits.py | 105 +++++++ regexsolver/_generated/models/boolean.py | 9 +- regexsolver/_generated/models/cardinality.py | 2 +- .../models/cardinality200_response.py | 9 +- .../models/cardinality_big_integer.py | 9 +- .../_generated/models/cardinality_infinite.py | 9 +- .../_generated/models/cardinality_integer.py | 9 +- .../_generated/models/concat200_response.py | 9 +- .../_generated/models/dot200_response.py | 9 +- .../_generated/models/empty200_response.py | 9 +- .../_generated/models/error_response.py | 9 +- .../_generated/models/error_response400.py | 9 +- .../_generated/models/error_response401.py | 9 +- .../_generated/models/error_response403.py | 9 +- .../_generated/models/execution_options.py | 9 +- .../models/fair_response_options.py | 9 +- .../generate_strings_character_order.py | 37 +++ .../models/generate_strings_path_order.py | 38 +++ .../models/generate_strings_request.py | 38 ++- .../models/generate_strings_response.py | 9 +- regexsolver/_generated/models/length.py | 9 +- .../_generated/models/length200_response.py | 9 +- .../_generated/models/limits200_response.py | 94 ++++++ .../_generated/models/multi_terms_request.py | 9 +- .../_generated/models/repeat_request.py | 9 +- .../_generated/models/request_options.py | 9 +- .../_generated/models/response_options.py | 9 +- regexsolver/_generated/models/string.py | 9 +- regexsolver/_generated/models/strings.py | 9 +- .../_generated/models/strings200_response.py | 9 +- regexsolver/_generated/models/term.py | 2 +- regexsolver/_generated/models/term_fair.py | 9 +- .../_generated/models/term_fair_metadata.py | 9 +- regexsolver/_generated/models/term_regex.py | 9 +- regexsolver/_generated/models/term_request.py | 9 +- .../_generated/models/two_terms_request.py | 9 +- regexsolver/_generated/rest.py | 2 +- regexsolver/clients/asynchronous.py | 271 +++++++++++++--- regexsolver/clients/rate_limiter.py | 98 ++---- regexsolver/clients/synchronous.py | 59 +++- regexsolver/models/account_limits.py | 49 +++ regexsolver/models/generate_order.py | 43 +++ tests/test_async_client.py | 285 ++++++++++++++++- tests/test_rate_limiter.py | 97 +++--- 58 files changed, 1545 insertions(+), 343 deletions(-) create mode 100644 regexsolver/_generated/api/account_api.py create mode 100644 regexsolver/_generated/models/account_limits.py create mode 100644 regexsolver/_generated/models/generate_strings_character_order.py create mode 100644 regexsolver/_generated/models/generate_strings_path_order.py create mode 100644 regexsolver/_generated/models/limits200_response.py create mode 100644 regexsolver/models/account_limits.py create mode 100644 regexsolver/models/generate_order.py diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index 5d6572d..3163f0c 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -1,6 +1,6 @@ -.gitignore regexsolver/_generated/__init__.py regexsolver/_generated/api/__init__.py +regexsolver/_generated/api/account_api.py regexsolver/_generated/api/analyze_api.py regexsolver/_generated/api/compute_api.py regexsolver/_generated/api/generate_api.py @@ -9,6 +9,7 @@ regexsolver/_generated/api_response.py regexsolver/_generated/configuration.py regexsolver/_generated/exceptions.py regexsolver/_generated/models/__init__.py +regexsolver/_generated/models/account_limits.py regexsolver/_generated/models/boolean.py regexsolver/_generated/models/cardinality.py regexsolver/_generated/models/cardinality200_response.py @@ -24,10 +25,13 @@ regexsolver/_generated/models/error_response401.py regexsolver/_generated/models/error_response403.py regexsolver/_generated/models/execution_options.py regexsolver/_generated/models/fair_response_options.py +regexsolver/_generated/models/generate_strings_character_order.py +regexsolver/_generated/models/generate_strings_path_order.py regexsolver/_generated/models/generate_strings_request.py regexsolver/_generated/models/generate_strings_response.py regexsolver/_generated/models/length.py regexsolver/_generated/models/length200_response.py +regexsolver/_generated/models/limits200_response.py regexsolver/_generated/models/multi_terms_request.py regexsolver/_generated/models/repeat_request.py regexsolver/_generated/models/request_options.py @@ -43,5 +47,3 @@ regexsolver/_generated/models/term_request.py regexsolver/_generated/models/two_terms_request.py regexsolver/_generated/py.typed regexsolver/_generated/rest.py -requirements.txt -test-requirements.txt diff --git a/.openapi-generator/VERSION b/.openapi-generator/VERSION index 2540a3a..a29ba3d 100644 --- a/.openapi-generator/VERSION +++ b/.openapi-generator/VERSION @@ -1 +1 @@ -7.20.0 +7.21.0 diff --git a/README.md b/README.md index 1fcc081..79dad81 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Timeout is best effort. The exact time is not guaranteed. ## API Overview -`RegexSolverClient` and `AsyncRegexSolverClient` expose the following methods. Every method accepts optional keyword arguments: operations that return a term take `response_format`, `deterministic` and `execution_timeout`, while analyze operations and `determinize()` take `execution_timeout` only — the response format is not theirs to choose. +`RegexSolverClient` and `AsyncRegexSolverClient` expose the following methods. Every method accepts optional keyword arguments: operations that return a term take `response_format`, `deterministic` and `execution_timeout`, while analyze operations and `determinize()` take `execution_timeout` only; the response format is not theirs to choose. `generate_strings()` additionally takes its ordering, seed, length and charset options as keyword arguments. ### Analyze @@ -125,7 +125,7 @@ Timeout is best effort. The exact time is not guaranteed. | `client.is_empty_string(term, **kwargs)` | `bool` | `True` if the term matches only the empty string. | | `client.is_total(term, **kwargs)` | `bool` | `True` if the term matches all possible strings. | | `client.is_deterministic(term, **kwargs)` | `bool` | `True` if the term's automaton is deterministic. Only a deterministic FAIR guarantees consistent string ordering across paginated `generate_strings()` calls; call `determinize()` first if this is `False`. | -| `client.subset(term1, term2, **kwargs)` | `bool` | `True` if every string matched by `term1` is also matched by `term2`. | +| `client.subset(term_subset, term_superset, **kwargs)` | `bool` | `True` if every string matched by `term_subset` is also matched by `term_superset`. | *Note: For `AsyncRegexSolverClient`, these methods are coroutines and must be awaited.* @@ -136,9 +136,9 @@ Timeout is best effort. The exact time is not guaranteed. | `client.complement(term, **kwargs)` | `Term` | Computes the complement of the given term. | | `client.concat(term1, term2, ..., **kwargs)` | `Term` | Concatenates multiple terms in order. | | `client.determinize(term, **kwargs)` | `Term` | Computes a deterministic FAIR for the given term, suitable for consistent pagination with `generate_strings()`. | -| `client.difference(term1, term2, **kwargs)` | `Term` | Computes the difference `term1 - term2`. | +| `client.difference(base_term, excluded_term, **kwargs)` | `Term` | Computes the difference `base_term - excluded_term`. | | `client.intersection(term1, term2, ..., **kwargs)` | `Term` | Computes the intersection of the given terms. | -| `client.repeat(term, min, max, **kwargs)` | `Term` | Computes the repetition of the term between `min` and `max` times. | +| `client.repeat(term, min_val, max_val, **kwargs)` | `Term` | Computes the repetition of the term between `min_val` and `max_val` times. | | `client.union(term1, term2, ..., **kwargs)` | `Term` | Computes the union of the given terms. | *Note: For `AsyncRegexSolverClient`, these methods are coroutines and must be awaited.* @@ -147,7 +147,7 @@ Timeout is best effort. The exact time is not guaranteed. | Method | Return | Description | | -------- | ------- | ------- | -| `client.generate_strings(term, limit, offset, **kwargs)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. | +| `client.generate_strings(term, limit, offset, **kwargs)` | `List[str]` | Generates up to `limit` unique strings matched by `term`, skipping the first `offset` strings. Keyword arguments control `path_order`, `character_order`, `seed`, `min_length`, `max_length` and `charset`. | *Note: For `AsyncRegexSolverClient`, this method is a coroutine and must be awaited.* diff --git a/regexsolver/__init__.py b/regexsolver/__init__.py index 0b372bb..1f58c90 100644 --- a/regexsolver/__init__.py +++ b/regexsolver/__init__.py @@ -22,7 +22,9 @@ TooManyTermsError, UnauthorizedError, ) +from regexsolver.models.account_limits import AccountLimits from regexsolver.models.cardinality import BigInteger, Cardinality, Infinite, Integer +from regexsolver.models.generate_order import CharacterOrder, PathOrder from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import FairTerm, RegexTerm, Term @@ -56,7 +58,10 @@ "BigInteger", "Infinite", "Integer", + "AccountLimits", "Cardinality", + "CharacterOrder", "Length", + "PathOrder", "ResponseFormat", ] diff --git a/regexsolver/_generated/__init__.py b/regexsolver/_generated/__init__.py index 177f216..3568daa 100644 --- a/regexsolver/_generated/__init__.py +++ b/regexsolver/_generated/__init__.py @@ -3,7 +3,7 @@ # flake8: noqa """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -18,6 +18,7 @@ # Define package exports __all__ = [ + "AccountApi", "AnalyzeApi", "ComputeApi", "GenerateApi", @@ -30,6 +31,7 @@ "ApiKeyError", "ApiAttributeError", "ApiException", + "AccountLimits", "Boolean", "Cardinality", "Cardinality200Response", @@ -45,10 +47,13 @@ "ErrorResponse403", "ExecutionOptions", "FairResponseOptions", + "GenerateStringsCharacterOrder", + "GenerateStringsPathOrder", "GenerateStringsRequest", "GenerateStringsResponse", "Length", "Length200Response", + "Limits200Response", "MultiTermsRequest", "RepeatRequest", "RequestOptions", @@ -65,6 +70,7 @@ ] # import apis into sdk package +from regexsolver._generated.api.account_api import AccountApi as AccountApi from regexsolver._generated.api.analyze_api import AnalyzeApi as AnalyzeApi from regexsolver._generated.api.compute_api import ComputeApi as ComputeApi from regexsolver._generated.api.generate_api import GenerateApi as GenerateApi @@ -81,6 +87,7 @@ from regexsolver._generated.exceptions import ApiException as ApiException # import models into sdk package +from regexsolver._generated.models.account_limits import AccountLimits as AccountLimits from regexsolver._generated.models.boolean import Boolean as Boolean from regexsolver._generated.models.cardinality import Cardinality as Cardinality from regexsolver._generated.models.cardinality200_response import Cardinality200Response as Cardinality200Response @@ -96,10 +103,13 @@ from regexsolver._generated.models.error_response403 import ErrorResponse403 as ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions as ExecutionOptions from regexsolver._generated.models.fair_response_options import FairResponseOptions as FairResponseOptions +from regexsolver._generated.models.generate_strings_character_order import GenerateStringsCharacterOrder as GenerateStringsCharacterOrder +from regexsolver._generated.models.generate_strings_path_order import GenerateStringsPathOrder as GenerateStringsPathOrder from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest as GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse as GenerateStringsResponse from regexsolver._generated.models.length import Length as Length from regexsolver._generated.models.length200_response import Length200Response as Length200Response +from regexsolver._generated.models.limits200_response import Limits200Response as Limits200Response from regexsolver._generated.models.multi_terms_request import MultiTermsRequest as MultiTermsRequest from regexsolver._generated.models.repeat_request import RepeatRequest as RepeatRequest from regexsolver._generated.models.request_options import RequestOptions as RequestOptions diff --git a/regexsolver/_generated/api/__init__.py b/regexsolver/_generated/api/__init__.py index aad0bc3..32a174c 100644 --- a/regexsolver/_generated/api/__init__.py +++ b/regexsolver/_generated/api/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa # import apis into api package +from regexsolver._generated.api.account_api import AccountApi from regexsolver._generated.api.analyze_api import AnalyzeApi from regexsolver._generated.api.compute_api import ComputeApi from regexsolver._generated.api.generate_api import GenerateApi diff --git a/regexsolver/_generated/api/account_api.py b/regexsolver/_generated/api/account_api.py new file mode 100644 index 0000000..dd716d6 --- /dev/null +++ b/regexsolver/_generated/api/account_api.py @@ -0,0 +1,293 @@ +""" + RegexSolver API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import warnings +from pydantic import validate_call, Field, StrictFloat, StrictStr, StrictInt +from typing import Any, Dict, List, Optional, Tuple, Union +from typing_extensions import Annotated + +from regexsolver._generated.models.limits200_response import Limits200Response + +from regexsolver._generated.api_client import ApiClient, RequestSerialized +from regexsolver._generated.api_response import ApiResponse +from regexsolver._generated.rest import RESTResponseType + + +class AccountApi: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client=None) -> None: + if api_client is None: + api_client = ApiClient.get_default() + self.api_client = api_client + + + @validate_call + async def limits( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> Limits200Response: + """Limits + + Return the plan limits applying to the account. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._limits_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Limits200Response", + '401': "ErrorResponse401", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ).data + + + @validate_call + async def limits_with_http_info( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> ApiResponse[Limits200Response]: + """Limits + + Return the plan limits applying to the account. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._limits_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Limits200Response", + '401': "ErrorResponse401", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + await response_data.read() + return self.api_client.response_deserialize( + response_data=response_data, + response_types_map=_response_types_map, + ) + + + @validate_call + async def limits_without_preload_content( + self, + _request_timeout: Union[ + None, + Annotated[StrictFloat, Field(gt=0)], + Tuple[ + Annotated[StrictFloat, Field(gt=0)], + Annotated[StrictFloat, Field(gt=0)] + ] + ] = None, + _request_auth: Optional[Dict[StrictStr, Any]] = None, + _content_type: Optional[StrictStr] = None, + _headers: Optional[Dict[StrictStr, Any]] = None, + _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, + ) -> RESTResponseType: + """Limits + + Return the plan limits applying to the account. + + :param _request_timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :type _request_timeout: int, tuple(int, int), optional + :param _request_auth: set to override the auth_settings for an a single + request; this effectively ignores the + authentication in the spec for a single request. + :type _request_auth: dict, optional + :param _content_type: force content-type for the request. + :type _content_type: str, Optional + :param _headers: set to override the headers for a single + request; this effectively ignores the headers + in the spec for a single request. + :type _headers: dict, optional + :param _host_index: set to override the host_index for a single + request; this effectively ignores the host_index + in the spec for a single request. + :type _host_index: int, optional + :return: Returns the result object. + """ # noqa: E501 + + _param = self._limits_serialize( + _request_auth=_request_auth, + _content_type=_content_type, + _headers=_headers, + _host_index=_host_index + ) + + _response_types_map: Dict[str, Optional[str]] = { + '200': "Limits200Response", + '401': "ErrorResponse401", + '404': "ErrorResponse", + '429': "ErrorResponse", + '500': "ErrorResponse", + } + response_data = await self.api_client.call_api( + *_param, + _request_timeout=_request_timeout + ) + return response_data.response + + + def _limits_serialize( + self, + _request_auth, + _content_type, + _headers, + _host_index, + ) -> RequestSerialized: + + _host = None + + _collection_formats: Dict[str, str] = { + } + + _path_params: Dict[str, str] = {} + _query_params: List[Tuple[str, str]] = [] + _header_params: Dict[str, Optional[str]] = _headers or {} + _form_params: List[Tuple[str, str]] = [] + _files: Dict[ + str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]] + ] = {} + _body_params: Optional[bytes] = None + + # process the path parameters + # process the query parameters + # process the header parameters + # process the form parameters + # process the body parameter + + + # set the HTTP header `Accept` + if 'Accept' not in _header_params: + _header_params['Accept'] = self.api_client.select_header_accept( + [ + 'application/json' + ] + ) + + + # authentication setting + _auth_settings: List[str] = [ + 'BearerAuth' + ] + + return self.api_client.param_serialize( + method='GET', + resource_path='/account/limits', + path_params=_path_params, + query_params=_query_params, + header_params=_header_params, + body=_body_params, + post_params=_form_params, + files=_files, + auth_settings=_auth_settings, + collection_formats=_collection_formats, + _host=_host, + _request_auth=_request_auth + ) + + diff --git a/regexsolver/_generated/api/analyze_api.py b/regexsolver/_generated/api/analyze_api.py index a64fae9..ab308d5 100644 --- a/regexsolver/_generated/api/analyze_api.py +++ b/regexsolver/_generated/api/analyze_api.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/_generated/api/compute_api.py b/regexsolver/_generated/api/compute_api.py index 64e6662..569b479 100644 --- a/regexsolver/_generated/api/compute_api.py +++ b/regexsolver/_generated/api/compute_api.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/_generated/api/generate_api.py b/regexsolver/_generated/api/generate_api.py index 8c10212..364d5b7 100644 --- a/regexsolver/_generated/api/generate_api.py +++ b/regexsolver/_generated/api/generate_api.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -55,7 +55,7 @@ async def strings( ) -> Strings200Response: """Strings - Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings, scheduling the paths of the language in `pathOrder`, producing the strings within each path in `characterOrder`, confined to lengths between `minLength` and `maxLength` and to the characters of `charset`. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -128,7 +128,7 @@ async def strings_with_http_info( ) -> ApiResponse[Strings200Response]: """Strings - Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings, scheduling the paths of the language in `pathOrder`, producing the strings within each path in `characterOrder`, confined to lengths between `minLength` and `maxLength` and to the characters of `charset`. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest @@ -201,7 +201,7 @@ async def strings_without_preload_content( ) -> RESTResponseType: """Strings - Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. + Generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings, scheduling the paths of the language in `pathOrder`, producing the strings within each path in `characterOrder`, confined to lengths between `minLength` and `maxLength` and to the characters of `charset`. Strings are only guaranteed to be distinct within a single call; pagination across calls is only consistent (no repeats or gaps) if `term` is deterministic. Call `/analyze/deterministic` to check, and `/compute/determinize` first if needed. :param generate_strings_request: (required) :type generate_strings_request: GenerateStringsRequest diff --git a/regexsolver/_generated/api_client.py b/regexsolver/_generated/api_client.py index 6fe5271..8c8549a 100644 --- a/regexsolver/_generated/api_client.py +++ b/regexsolver/_generated/api_client.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -68,6 +68,7 @@ class ApiClient: 'date': datetime.date, 'datetime': datetime.datetime, 'decimal': decimal.Decimal, + 'UUID': uuid.UUID, 'object': object, } _pool = None @@ -308,7 +309,7 @@ def response_deserialize( response_text = None return_data = None try: - if response_type == "bytearray": + if response_type in ("bytearray", "bytes"): return_data = response_data.data elif response_type == "file": return_data = self.__deserialize_file(response_data) @@ -470,6 +471,8 @@ def __deserialize(self, data, klass): return self.__deserialize_datetime(data) elif klass is decimal.Decimal: return decimal.Decimal(data) + elif klass is uuid.UUID: + return uuid.UUID(data) elif issubclass(klass, Enum): return self.__deserialize_enum(data, klass) else: diff --git a/regexsolver/_generated/configuration.py b/regexsolver/_generated/configuration.py index 61f2d2f..a2d0239 100644 --- a/regexsolver/_generated/configuration.py +++ b/regexsolver/_generated/configuration.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -155,6 +155,8 @@ class Configuration: string values to replace variables in templated server configuration. The validation of enums is performed for variables with defined enum values before. + :param verify_ssl: bool - Set this to false to skip verifying SSL certificate + when calling API from https server. :param ssl_ca_cert: str - the path to a file of concatenated CA certificates in PEM format. :param retries: int | aiohttp_retry.RetryOptionsBase - Retry configuration. @@ -162,6 +164,16 @@ class Configuration: in PEM (str) or DER (bytes) format. :param cert_file: the path to a client certificate file, for mTLS. :param key_file: the path to a client key file, for mTLS. + :param assert_hostname: Set this to True/False to enable/disable SSL hostname verification. + :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. + :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. + :param proxy: Proxy URL. + :param proxy_headers: Proxy headers. + :param safe_chars_for_path_param: Safe characters for path parameter encoding. + :param client_side_validation: Enable client-side validation. Default True. + :param socket_options: Options to pass down to the underlying urllib3 socket. + :param datetime_format: Datetime format string for serialization. + :param date_format: Date format string for serialization. :Example: """ @@ -186,6 +198,17 @@ def __init__( ca_cert_data: Optional[Union[str, bytes]] = None, cert_file: Optional[str]=None, key_file: Optional[str]=None, + verify_ssl: bool=True, + assert_hostname: Optional[bool]=None, + tls_server_name: Optional[str]=None, + connection_pool_maxsize: Optional[int]=None, + proxy: Optional[str]=None, + proxy_headers: Optional[Any]=None, + safe_chars_for_path_param: str='', + client_side_validation: bool=True, + socket_options: Optional[Any]=None, + datetime_format: str="%Y-%m-%dT%H:%M:%S.%f%z", + date_format: str="%Y-%m-%d", *, debug: Optional[bool] = None, ) -> None: @@ -254,7 +277,7 @@ def __init__( """Debug switch """ - self.verify_ssl = True + self.verify_ssl = verify_ssl """SSL/TLS verification Set this to false to skip verifying SSL certificate when calling API from https server. @@ -272,43 +295,43 @@ def __init__( self.key_file = key_file """client key file """ - self.assert_hostname = None + self.assert_hostname = assert_hostname """Set this to True/False to enable/disable SSL hostname verification. """ - self.tls_server_name = None + self.tls_server_name = tls_server_name """SSL/TLS Server Name Indication (SNI) Set this to the SNI value expected by the server. """ - self.connection_pool_maxsize = 100 + self.connection_pool_maxsize = connection_pool_maxsize if connection_pool_maxsize is not None else 100 """This value is passed to the aiohttp to limit simultaneous connections. - Default values is 100, None means no-limit. + None in the constructor is coerced to default 100. """ - self.proxy: Optional[str] = None + self.proxy = proxy """Proxy URL """ - self.proxy_headers = None + self.proxy_headers = proxy_headers """Proxy headers """ - self.safe_chars_for_path_param = '' + self.safe_chars_for_path_param = safe_chars_for_path_param """Safe chars for path_param """ self.retries = retries """Retry configuration """ # Enable client side validation - self.client_side_validation = True + self.client_side_validation = client_side_validation - self.socket_options = None + self.socket_options = socket_options """Options to pass down to the underlying urllib3 socket """ - self.datetime_format = "%Y-%m-%dT%H:%M:%S.%f%z" + self.datetime_format = datetime_format """datetime format """ - self.date_format = "%Y-%m-%d" + self.date_format = date_format """date format """ diff --git a/regexsolver/_generated/exceptions.py b/regexsolver/_generated/exceptions.py index c5e16e9..5846240 100644 --- a/regexsolver/_generated/exceptions.py +++ b/regexsolver/_generated/exceptions.py @@ -1,5 +1,5 @@ """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/_generated/models/__init__.py b/regexsolver/_generated/models/__init__.py index cc1b550..77f658d 100644 --- a/regexsolver/_generated/models/__init__.py +++ b/regexsolver/_generated/models/__init__.py @@ -2,7 +2,7 @@ # flake8: noqa """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -13,6 +13,7 @@ """ # noqa: E501 # import models into model package +from regexsolver._generated.models.account_limits import AccountLimits from regexsolver._generated.models.boolean import Boolean from regexsolver._generated.models.cardinality import Cardinality from regexsolver._generated.models.cardinality200_response import Cardinality200Response @@ -28,10 +29,13 @@ from regexsolver._generated.models.error_response403 import ErrorResponse403 from regexsolver._generated.models.execution_options import ExecutionOptions from regexsolver._generated.models.fair_response_options import FairResponseOptions +from regexsolver._generated.models.generate_strings_character_order import GenerateStringsCharacterOrder +from regexsolver._generated.models.generate_strings_path_order import GenerateStringsPathOrder from regexsolver._generated.models.generate_strings_request import GenerateStringsRequest from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse from regexsolver._generated.models.length import Length from regexsolver._generated.models.length200_response import Length200Response +from regexsolver._generated.models.limits200_response import Limits200Response from regexsolver._generated.models.multi_terms_request import MultiTermsRequest from regexsolver._generated.models.repeat_request import RepeatRequest from regexsolver._generated.models.request_options import RequestOptions diff --git a/regexsolver/_generated/models/account_limits.py b/regexsolver/_generated/models/account_limits.py new file mode 100644 index 0000000..9d9e1f0 --- /dev/null +++ b/regexsolver/_generated/models/account_limits.py @@ -0,0 +1,105 @@ +# coding: utf-8 + +""" + RegexSolver API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, field_validator +from typing import Any, ClassVar, Dict, List +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class AccountLimits(BaseModel): + """ + The plan limits currently applying to the account. + """ # noqa: E501 + type: StrictStr + max_requests_count: StrictInt = Field(description="Maximum number of requests allowed per billing period.", alias="maxRequestsCount") + max_requests_rate: StrictInt = Field(description="Maximum number of requests allowed per second. `0` means no rate limit is enforced.", alias="maxRequestsRate") + max_terms_count: StrictInt = Field(description="Maximum number of terms accepted in a single request.", alias="maxTermsCount") + max_timeout: StrictInt = Field(description="Maximum execution timeout per request, in milliseconds.", alias="maxTimeout") + max_states_count: StrictInt = Field(description="Maximum number of automaton states an operation may build.", alias="maxStatesCount") + __properties: ClassVar[List[str]] = ["type", "maxRequestsCount", "maxRequestsRate", "maxTermsCount", "maxTimeout", "maxStatesCount"] + + @field_validator('type') + def type_validate_enum(cls, value): + """Validates the enum""" + if value not in set(['accountLimits']): + raise ValueError("must be one of enum values ('accountLimits')") + return value + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AccountLimits from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AccountLimits from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "type": obj.get("type"), + "maxRequestsCount": obj.get("maxRequestsCount"), + "maxRequestsRate": obj.get("maxRequestsRate"), + "maxTermsCount": obj.get("maxTermsCount"), + "maxTimeout": obj.get("maxTimeout"), + "maxStatesCount": obj.get("maxStatesCount") + }) + return _obj + + diff --git a/regexsolver/_generated/models/boolean.py b/regexsolver/_generated/models/boolean.py index 4aa2a4f..8af12ad 100644 --- a/regexsolver/_generated/models/boolean.py +++ b/regexsolver/_generated/models/boolean.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Boolean(BaseModel): """ @@ -38,7 +39,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -50,8 +52,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/cardinality.py b/regexsolver/_generated/models/cardinality.py index c70ebec..aca5f20 100644 --- a/regexsolver/_generated/models/cardinality.py +++ b/regexsolver/_generated/models/cardinality.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/_generated/models/cardinality200_response.py b/regexsolver/_generated/models/cardinality200_response.py index 0351a02..41527de 100644 --- a/regexsolver/_generated/models/cardinality200_response.py +++ b/regexsolver/_generated/models/cardinality200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.cardinality import Cardinality from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Cardinality200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Cardinality200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/cardinality_big_integer.py b/regexsolver/_generated/models/cardinality_big_integer.py index 0a94022..f5997f6 100644 --- a/regexsolver/_generated/models/cardinality_big_integer.py +++ b/regexsolver/_generated/models/cardinality_big_integer.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class CardinalityBigInteger(BaseModel): """ @@ -37,7 +38,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -49,8 +51,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/cardinality_infinite.py b/regexsolver/_generated/models/cardinality_infinite.py index 9db9135..4446c6c 100644 --- a/regexsolver/_generated/models/cardinality_infinite.py +++ b/regexsolver/_generated/models/cardinality_infinite.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class CardinalityInfinite(BaseModel): """ @@ -37,7 +38,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -49,8 +51,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/cardinality_integer.py b/regexsolver/_generated/models/cardinality_integer.py index 9952680..07a3bd9 100644 --- a/regexsolver/_generated/models/cardinality_integer.py +++ b/regexsolver/_generated/models/cardinality_integer.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class CardinalityInteger(BaseModel): """ @@ -39,7 +40,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -51,8 +53,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/concat200_response.py b/regexsolver/_generated/models/concat200_response.py index 71934b4..019dfcd 100644 --- a/regexsolver/_generated/models/concat200_response.py +++ b/regexsolver/_generated/models/concat200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Concat200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Concat200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/dot200_response.py b/regexsolver/_generated/models/dot200_response.py index e3b4966..28986a9 100644 --- a/regexsolver/_generated/models/dot200_response.py +++ b/regexsolver/_generated/models/dot200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.string import String from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Dot200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Dot200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/empty200_response.py b/regexsolver/_generated/models/empty200_response.py index 85e7b89..8722e0c 100644 --- a/regexsolver/_generated/models/empty200_response.py +++ b/regexsolver/_generated/models/empty200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.boolean import Boolean from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Empty200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Empty200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/error_response.py b/regexsolver/_generated/models/error_response.py index fe998e1..a050b28 100644 --- a/regexsolver/_generated/models/error_response.py +++ b/regexsolver/_generated/models/error_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ErrorResponse(BaseModel): """ @@ -32,7 +33,8 @@ class ErrorResponse(BaseModel): __properties: ClassVar[List[str]] = ["success", "error", "errorCode"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/error_response400.py b/regexsolver/_generated/models/error_response400.py index 7cd4626..613bdd7 100644 --- a/regexsolver/_generated/models/error_response400.py +++ b/regexsolver/_generated/models/error_response400.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ErrorResponse400(BaseModel): """ @@ -42,7 +43,8 @@ def error_code_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -54,8 +56,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/error_response401.py b/regexsolver/_generated/models/error_response401.py index 5d30374..ead4c50 100644 --- a/regexsolver/_generated/models/error_response401.py +++ b/regexsolver/_generated/models/error_response401.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ErrorResponse401(BaseModel): """ @@ -42,7 +43,8 @@ def error_code_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -54,8 +56,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/error_response403.py b/regexsolver/_generated/models/error_response403.py index d8bb8ae..8aeba51 100644 --- a/regexsolver/_generated/models/error_response403.py +++ b/regexsolver/_generated/models/error_response403.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ErrorResponse403(BaseModel): """ @@ -42,7 +43,8 @@ def error_code_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -54,8 +56,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/execution_options.py b/regexsolver/_generated/models/execution_options.py index e75b7ec..087d724 100644 --- a/regexsolver/_generated/models/execution_options.py +++ b/regexsolver/_generated/models/execution_options.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ExecutionOptions(BaseModel): """ @@ -31,7 +32,8 @@ class ExecutionOptions(BaseModel): __properties: ClassVar[List[str]] = ["timeout"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -43,8 +45,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/fair_response_options.py b/regexsolver/_generated/models/fair_response_options.py index 780ba17..5ef2b09 100644 --- a/regexsolver/_generated/models/fair_response_options.py +++ b/regexsolver/_generated/models/fair_response_options.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class FairResponseOptions(BaseModel): """ @@ -30,7 +31,8 @@ class FairResponseOptions(BaseModel): __properties: ClassVar[List[str]] = ["deterministic"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -42,8 +44,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/generate_strings_character_order.py b/regexsolver/_generated/models/generate_strings_character_order.py new file mode 100644 index 0000000..75acf10 --- /dev/null +++ b/regexsolver/_generated/models/generate_strings_character_order.py @@ -0,0 +1,37 @@ +# coding: utf-8 + +""" + RegexSolver API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class GenerateStringsCharacterOrder(str, Enum): + """ + Order in which the strings within each path are produced. Orthogonal to `pathOrder`: it does not change *what* can be generated, only which strings are reached first. `ascending` expands each position from the low end of its character range first, so `[a-z]{8}` yields `aaaaaaaa`, `aaaaaaab`, ... — a stable, spec-defined order returning the smallest witnesses of a path first. `shuffled` applies a permutation drawn from `seed`, so `[a-z]{8}` yields something like `sjtwsive` instead: the strings look like real inputs. Random in look only — generation stays reproducible and pages with `offset`, though offsets are only consistent between calls sharing the same `seed`, and the exact sequence may change between releases. Use `charset` to restrict generation to specific characters. + """ + + """ + allowed enum values + """ + ASCENDING = 'ascending' + SHUFFLED = 'shuffled' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of GenerateStringsCharacterOrder from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regexsolver/_generated/models/generate_strings_path_order.py b/regexsolver/_generated/models/generate_strings_path_order.py new file mode 100644 index 0000000..275b5bd --- /dev/null +++ b/regexsolver/_generated/models/generate_strings_path_order.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +""" + RegexSolver API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import json +from enum import Enum +from typing_extensions import Self + + +class GenerateStringsPathOrder(str, Enum): + """ + Order in which the paths of the language are scheduled — the *shapes* the term allows, as opposed to the characters filling them (`characterOrder`). `sweep` expands one path in full, shortest first, before moving to the next one: the cheapest way to page through a whole language with `offset`. `interleave` covers every path the term holds before any path is asked for a second string, so a `limit` smaller than the number of shapes is spent entirely on distinct shapes; slower than `sweep`, but better suited to deriving test cases. `shuffled` is `interleave` with same-length paths visited in an order drawn by `seed`. Shorter paths still come first, so the seed only draws among paths of equal length. All three are deterministic and page with `offset`; for `shuffled`, offsets are only consistent between calls sharing the same `seed`. + """ + + """ + allowed enum values + """ + SWEEP = 'sweep' + INTERLEAVE = 'interleave' + SHUFFLED = 'shuffled' + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Create an instance of GenerateStringsPathOrder from a JSON string""" + return cls(json.loads(json_str)) + + diff --git a/regexsolver/_generated/models/generate_strings_request.py b/regexsolver/_generated/models/generate_strings_request.py index 544bc33..1dcf3cc 100644 --- a/regexsolver/_generated/models/generate_strings_request.py +++ b/regexsolver/_generated/models/generate_strings_request.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -17,26 +17,36 @@ import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, StrictStr from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated +from regexsolver._generated.models.generate_strings_character_order import GenerateStringsCharacterOrder +from regexsolver._generated.models.generate_strings_path_order import GenerateStringsPathOrder from regexsolver._generated.models.request_options import RequestOptions from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class GenerateStringsRequest(BaseModel): """ - Request to generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. For consistent pagination, `term` should be deterministic. + Request to generate up to `limit` distinct strings matched by `term`, skipping the first `offset` strings and confined to lengths between `minLength` and `maxLength`. For consistent pagination, `term` should be deterministic. """ # noqa: E501 term: Term = Field(description="Source term to generate strings from.") limit: Annotated[int, Field(le=100, strict=True, ge=1)] = Field(description="Maximum number of unique strings to return.") - offset: Annotated[int, Field(strict=True, ge=0)] = Field(description="Number of matched strings to skip before starting to collect the results. Used for pagination.") + offset: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=0, description="Number of matched strings to skip before starting to collect the results. Used for pagination.") + min_length: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=0, description="Shortest string to generate. Strings shorter than this are left out of the enumeration entirely, `offset` never counting them.", alias="minLength") + max_length: Optional[Annotated[int, Field(le=100, strict=True, ge=1)]] = Field(default=100, description="Longest string to generate. Strings longer than this are left out of the enumeration entirely, `offset` never counting them. A value below `minLength` leaves nothing to generate.", alias="maxLength") + path_order: Optional[GenerateStringsPathOrder] = Field(default=None, description="Order in which the paths of the language are scheduled. Defaults to `sweep`.", alias="pathOrder") + character_order: Optional[GenerateStringsCharacterOrder] = Field(default=None, description="Order in which the strings within each path are produced. Defaults to `ascending`.", alias="characterOrder") + seed: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=0, description="Seed behind the `shuffled` modes of `pathOrder` and `characterOrder`; ignored when neither is used. The default seed is fixed rather than random, so two calls sharing a seed generate the same strings and `offset` pages through them consistently. Change it to draw a different sequence from the same term.") + charset: Optional[StrictStr] = Field(default=None, description="Character class the generated strings are restricted to, such as `[a-z]` or `\\P{C}`. Paths needing a character outside of it are dropped entirely. If omitted, every character the term allows is used.") options: Optional[RequestOptions] = None - __properties: ClassVar[List[str]] = ["term", "limit", "offset", "options"] + __properties: ClassVar[List[str]] = ["term", "limit", "offset", "minLength", "maxLength", "pathOrder", "characterOrder", "seed", "charset", "options"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -48,8 +58,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: @@ -80,6 +89,11 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of options if self.options: _dict['options'] = self.options.to_dict() + # set to None if charset (nullable) is None + # and model_fields_set contains the field + if self.charset is None and "charset" in self.model_fields_set: + _dict['charset'] = None + return _dict @classmethod @@ -94,7 +108,13 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "term": Term.from_dict(obj["term"]) if obj.get("term") is not None else None, "limit": obj.get("limit"), - "offset": obj.get("offset"), + "offset": obj.get("offset") if obj.get("offset") is not None else 0, + "minLength": obj.get("minLength") if obj.get("minLength") is not None else 0, + "maxLength": obj.get("maxLength") if obj.get("maxLength") is not None else 100, + "pathOrder": obj.get("pathOrder"), + "characterOrder": obj.get("characterOrder"), + "seed": obj.get("seed") if obj.get("seed") is not None else 0, + "charset": obj.get("charset"), "options": RequestOptions.from_dict(obj["options"]) if obj.get("options") is not None else None }) return _obj diff --git a/regexsolver/_generated/models/generate_strings_response.py b/regexsolver/_generated/models/generate_strings_response.py index c5d0843..1050c56 100644 --- a/regexsolver/_generated/models/generate_strings_response.py +++ b/regexsolver/_generated/models/generate_strings_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.strings import Strings from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class GenerateStringsResponse(BaseModel): """ @@ -39,7 +40,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -51,8 +53,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/length.py b/regexsolver/_generated/models/length.py index 51de676..5392fa9 100644 --- a/regexsolver/_generated/models/length.py +++ b/regexsolver/_generated/models/length.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Length(BaseModel): """ @@ -39,7 +40,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -51,8 +53,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/length200_response.py b/regexsolver/_generated/models/length200_response.py index a78c824..e2df0fd 100644 --- a/regexsolver/_generated/models/length200_response.py +++ b/regexsolver/_generated/models/length200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.length import Length from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Length200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Length200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/limits200_response.py b/regexsolver/_generated/models/limits200_response.py new file mode 100644 index 0000000..05bb854 --- /dev/null +++ b/regexsolver/_generated/models/limits200_response.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + RegexSolver API + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.1.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictBool +from typing import Any, ClassVar, Dict, List +from regexsolver._generated.models.account_limits import AccountLimits +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class Limits200Response(BaseModel): + """ + Limits200Response + """ # noqa: E501 + success: StrictBool + data: AccountLimits + __properties: ClassVar[List[str]] = ["success", "data"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of Limits200Response from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of data + if self.data: + _dict['data'] = self.data.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of Limits200Response from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "success": obj.get("success"), + "data": AccountLimits.from_dict(obj["data"]) if obj.get("data") is not None else None + }) + return _obj + + diff --git a/regexsolver/_generated/models/multi_terms_request.py b/regexsolver/_generated/models/multi_terms_request.py index dcf331c..809ad84 100644 --- a/regexsolver/_generated/models/multi_terms_request.py +++ b/regexsolver/_generated/models/multi_terms_request.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -24,6 +24,7 @@ from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class MultiTermsRequest(BaseModel): """ @@ -34,7 +35,8 @@ class MultiTermsRequest(BaseModel): __properties: ClassVar[List[str]] = ["terms", "options"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -46,8 +48,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/repeat_request.py b/regexsolver/_generated/models/repeat_request.py index f7b506f..bac18f2 100644 --- a/regexsolver/_generated/models/repeat_request.py +++ b/regexsolver/_generated/models/repeat_request.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -24,6 +24,7 @@ from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class RepeatRequest(BaseModel): """ @@ -36,7 +37,8 @@ class RepeatRequest(BaseModel): __properties: ClassVar[List[str]] = ["term", "min", "max", "options"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -48,8 +50,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/request_options.py b/regexsolver/_generated/models/request_options.py index fcecf97..8930b0d 100644 --- a/regexsolver/_generated/models/request_options.py +++ b/regexsolver/_generated/models/request_options.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -23,6 +23,7 @@ from regexsolver._generated.models.response_options import ResponseOptions from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class RequestOptions(BaseModel): """ @@ -34,7 +35,8 @@ class RequestOptions(BaseModel): __properties: ClassVar[List[str]] = ["schemaVersion", "response", "execution"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -46,8 +48,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/response_options.py b/regexsolver/_generated/models/response_options.py index 9caf806..4fe4b7e 100644 --- a/regexsolver/_generated/models/response_options.py +++ b/regexsolver/_generated/models/response_options.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.fair_response_options import FairResponseOptions from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class ResponseOptions(BaseModel): """ @@ -42,7 +43,8 @@ def format_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -54,8 +56,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/string.py b/regexsolver/_generated/models/string.py index c003aa4..6ec5858 100644 --- a/regexsolver/_generated/models/string.py +++ b/regexsolver/_generated/models/string.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class String(BaseModel): """ @@ -38,7 +39,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -50,8 +52,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/strings.py b/regexsolver/_generated/models/strings.py index e69551c..2040c68 100644 --- a/regexsolver/_generated/models/strings.py +++ b/regexsolver/_generated/models/strings.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Strings(BaseModel): """ @@ -38,7 +39,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -50,8 +52,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/strings200_response.py b/regexsolver/_generated/models/strings200_response.py index 56ffb8e..0122f98 100644 --- a/regexsolver/_generated/models/strings200_response.py +++ b/regexsolver/_generated/models/strings200_response.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.generate_strings_response import GenerateStringsResponse from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class Strings200Response(BaseModel): """ @@ -32,7 +33,8 @@ class Strings200Response(BaseModel): __properties: ClassVar[List[str]] = ["success", "data"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -44,8 +46,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/term.py b/regexsolver/_generated/models/term.py index 028810c..e817cdc 100644 --- a/regexsolver/_generated/models/term.py +++ b/regexsolver/_generated/models/term.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/_generated/models/term_fair.py b/regexsolver/_generated/models/term_fair.py index 412ca39..33a1d00 100644 --- a/regexsolver/_generated/models/term_fair.py +++ b/regexsolver/_generated/models/term_fair.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -22,6 +22,7 @@ from regexsolver._generated.models.term_fair_metadata import TermFairMetadata from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class TermFair(BaseModel): """ @@ -40,7 +41,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -52,8 +54,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/term_fair_metadata.py b/regexsolver/_generated/models/term_fair_metadata.py index c62e27d..bbaf575 100644 --- a/regexsolver/_generated/models/term_fair_metadata.py +++ b/regexsolver/_generated/models/term_fair_metadata.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class TermFairMetadata(BaseModel): """ @@ -30,7 +31,8 @@ class TermFairMetadata(BaseModel): __properties: ClassVar[List[str]] = ["deterministic"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -42,8 +44,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/term_regex.py b/regexsolver/_generated/models/term_regex.py index 892cbc7..b05ba1a 100644 --- a/regexsolver/_generated/models/term_regex.py +++ b/regexsolver/_generated/models/term_regex.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -21,6 +21,7 @@ from typing import Any, ClassVar, Dict, List from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class TermRegex(BaseModel): """ @@ -38,7 +39,8 @@ def type_validate_enum(cls, value): return value model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -50,8 +52,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/term_request.py b/regexsolver/_generated/models/term_request.py index bd83624..920e644 100644 --- a/regexsolver/_generated/models/term_request.py +++ b/regexsolver/_generated/models/term_request.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -23,6 +23,7 @@ from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class TermRequest(BaseModel): """ @@ -33,7 +34,8 @@ class TermRequest(BaseModel): __properties: ClassVar[List[str]] = ["term", "options"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -45,8 +47,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/models/two_terms_request.py b/regexsolver/_generated/models/two_terms_request.py index 4eb81d1..e84c0c3 100644 --- a/regexsolver/_generated/models/two_terms_request.py +++ b/regexsolver/_generated/models/two_terms_request.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) @@ -24,6 +24,7 @@ from regexsolver._generated.models.term import Term from typing import Optional, Set from typing_extensions import Self +from pydantic_core import to_jsonable_python class TwoTermsRequest(BaseModel): """ @@ -34,7 +35,8 @@ class TwoTermsRequest(BaseModel): __properties: ClassVar[List[str]] = ["terms", "options"] model_config = ConfigDict( - populate_by_name=True, + validate_by_name=True, + validate_by_alias=True, validate_assignment=True, protected_namespaces=(), ) @@ -46,8 +48,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return json.dumps(to_jsonable_python(self.to_dict())) @classmethod def from_json(cls, json_str: str) -> Optional[Self]: diff --git a/regexsolver/_generated/rest.py b/regexsolver/_generated/rest.py index 93a8f1a..094286e 100644 --- a/regexsolver/_generated/rest.py +++ b/regexsolver/_generated/rest.py @@ -1,7 +1,7 @@ # coding: utf-8 """ - RegexSolver + RegexSolver API No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) diff --git a/regexsolver/clients/asynchronous.py b/regexsolver/clients/asynchronous.py index 37a374f..a64d750 100644 --- a/regexsolver/clients/asynchronous.py +++ b/regexsolver/clients/asynchronous.py @@ -1,11 +1,14 @@ import asyncio import logging +import random +import time import weakref -from typing import List, Optional, Union +from typing import Awaitable, Callable, List, Optional, Union from pydantic import ValidationError from regexsolver._generated import ( + AccountApi, AnalyzeApi, ApiClient, ApiException, @@ -46,13 +49,35 @@ TooManyTermsError, UnauthorizedError, ) +from regexsolver.models.account_limits import AccountLimits from regexsolver.models.cardinality import Cardinality, Infinite, Integer +from regexsolver.models.generate_order import CharacterOrder, PathOrder from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import FairTerm, Term logger = logging.getLogger(__name__) +# Retry policy for 429 responses: retry as long as the total wait stays +# within the budget, adding full jitter on top of `Retry-After` so concurrent +# waiters do not re-collide as a single burst. The values are shared across +# all the official clients — change them together. +_RETRY_BUDGET_S = 300.0 +_JITTER_BASE_S = 0.25 +_JITTER_CAP_S = 2.0 +_DEFAULT_RETRY_AFTER_S = 1.0 + + +def _get_retry_after(headers) -> float: + """Case-insensitively read the Retry-After header, in seconds.""" + for key, value in (headers or {}).items(): + if str(key).lower() == "retry-after": + try: + return float(value) + except (TypeError, ValueError): + break + return _DEFAULT_RETRY_AFTER_S + def _build_request(model, **kwargs): """Build a generated request model, keeping pydantic out of the public surface. @@ -91,18 +116,36 @@ class AsyncRegexSolverClient: Can be used as a standalone object or as an `async with` context manager. """ - def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + def __init__( + self, + api_token: str, + base_url: str = "https://api.regexsolver.com/v1", + auto_batch: bool = True, + max_terms_per_request: Optional[int] = None, + ): + if not api_token: + raise ValueError("api_token is required") + if max_terms_per_request is not None and max_terms_per_request < 2: + raise ValueError("max_terms_per_request must be at least 2") + logger.debug("Initializing AsyncRegexSolverClient.") self.configuration = Configuration(host=base_url, access_token=api_token) self.api_client = ApiClient(self.configuration) self.api_client.user_agent = "RegexSolver Python / 1.1.0" + self._account_api = AccountApi(self.api_client) self._analyze_api = AnalyzeApi(self.api_client) self._compute_api = ComputeApi(self.api_client) self._generate_api = GenerateApi(self.api_client) self._rate_limiter = get_rate_limiter(api_token) + self._auto_batch = auto_batch + self._max_terms_per_request = max_terms_per_request + self._limits: Optional[AccountLimits] = None + # Created lazily: asyncio primitives must be born on the running loop. + self._limits_lock: Optional[asyncio.Lock] = None + # Ensure the underlying aiohttp session is closed when the client is GC'd. self._finalizer = weakref.finalize(self, self._run_cleanup, self.api_client) @@ -136,27 +179,33 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): # --- HELPER --- async def _execute_with_retry(self, api_method, **kwargs): - retried = False + attempt = 0 + first_failure_at: Optional[float] = None while True: await self._rate_limiter.wait() + if attempt > 0: + await asyncio.sleep( + random.uniform(0.0, min(_JITTER_BASE_S * 2**attempt, _JITTER_CAP_S)) + ) try: return await api_method(**kwargs) except ApiException as e: - if e.status == 429: - if retried: - raise self._map_error(e) - - retried = True - headers = e.headers or {} - retry_after = float(headers.get("Retry-After", 1)) - logger.debug( - "429 Too Many Requests hit. " - f"Triggering rate limiter for {retry_after} seconds." - ) - await self._rate_limiter.trigger(retry_after) - continue - - raise self._map_error(e) + if e.status != 429: + raise self._map_error(e) + + retry_after = _get_retry_after(e.headers) + now = time.monotonic() + if first_failure_at is None: + first_failure_at = now + if now - first_failure_at + retry_after > _RETRY_BUDGET_S: + raise self._map_error(e) + + logger.debug( + "429 Too Many Requests hit. " + f"Triggering rate limiter for {retry_after} seconds." + ) + self._rate_limiter.trigger(retry_after) + attempt += 1 def _map_error(self, e: ApiException) -> Exception: status_code = e.status @@ -281,6 +330,107 @@ def _build_options( options.response = response_opts return options + # --- ACCOUNT --- + async def get_account_limits(self) -> AccountLimits: + """Fetches the plan limits applying to the account. + + The call never consumes request quota (it is only rate-limited) and + the result is cached on the client, so calling it again is free. The + cached `max_terms_count` also drives auto-batching. + + Returns: + AccountLimits: The five plan limits. + """ + if self._limits is not None: + return self._limits + if self._limits_lock is None: + self._limits_lock = asyncio.Lock() + async with self._limits_lock: + if self._limits is None: + response = await self._execute_with_retry(self._account_api.limits) + self._limits = AccountLimits.from_dto(response.data) + return self._limits + + # --- BATCHING --- + def _effective_max_terms(self) -> Optional[int]: + """The largest term count to send in one request, when known.""" + server_max = self._limits.max_terms_count if self._limits else None + if self._max_terms_per_request is not None: + if server_max is not None: + return min(self._max_terms_per_request, server_max) + return self._max_terms_per_request + return server_max + + async def _run_nary( + self, + api_method, + terms, + response_format: Optional[Union[ResponseFormat, str]], + deterministic: Optional[bool], + execution_timeout: Optional[int], + ) -> Term: + """Run an n-ary operation (concat/intersection/union), transparently + splitting the terms into several requests when they exceed the + account's terms-per-request limit (auto-batching). + """ + terms = list(terms) + + async def call(batch: List[Term], final: bool) -> Term: + # Intermediate results are fed straight back into the next + # request, so only the final call carries the caller's response + # options; execution_timeout bounds every constituent request. + request = _build_request( + MultiTermsRequest, + terms=[t.to_dto() for t in batch], + options=self._build_options( + execution_timeout, + response_format if final else None, + deterministic if final else None, + ), + ) + response = await self._execute_with_retry( + api_method, multi_terms_request=request + ) + return Term.from_dto(response.data) + + max_terms = self._effective_max_terms() if self._auto_batch else None + if max_terms is not None and len(terms) > max_terms: + return await self._fold(call, terms, max_terms) + + try: + return await call(terms, True) + except TooManyTermsError as too_many: + if not self._auto_batch or max_terms is not None: + raise + try: + await self.get_account_limits() + except RegexSolverError as fetch_error: + logger.debug(f"Fetching account limits failed: {fetch_error}") + raise too_many from None + max_terms = self._effective_max_terms() + if max_terms is None or max_terms < 2 or len(terms) <= max_terms: + raise + return await self._fold(call, terms, max_terms) + + @staticmethod + async def _fold( + call: Callable[[List[Term], bool], Awaitable[Term]], + terms: List[Term], + max_terms: int, + ) -> Term: + """Left fold: combine the first `max_terms` terms, then keep feeding + the accumulated result back with the next `max_terms - 1` terms. + Left-associative, so `concat` order is preserved; `union` and + `intersection` are commutative and unaffected. + """ + acc = await call(terms[:max_terms], False) + index = max_terms + while index < len(terms): + batch = [acc] + terms[index: index + max_terms - 1] + index += max_terms - 1 + acc = await call(batch, index >= len(terms)) + return acc + # --- ANALYZE --- async def get_cardinality( self, term: Term, execution_timeout: Optional[int] = None @@ -563,17 +713,13 @@ async def concat( Returns: Term: A newly computed concatenated term. """ - request = _build_request( - MultiTermsRequest, - terms=[t.to_dto() for t in terms], - options=self._build_options( - execution_timeout, response_format, deterministic - ), + return await self._run_nary( + self._compute_api.concat, + terms, + response_format, + deterministic, + execution_timeout, ) - response = await self._execute_with_retry( - self._compute_api.concat, multi_terms_request=request - ) - return Term.from_dto(response.data) async def intersection( self, @@ -595,17 +741,13 @@ async def intersection( Returns: Term: A term representing only strings matched by ALL provided terms. """ - request = _build_request( - MultiTermsRequest, - terms=[t.to_dto() for t in terms], - options=self._build_options( - execution_timeout, response_format, deterministic - ), + return await self._run_nary( + self._compute_api.intersection, + terms, + response_format, + deterministic, + execution_timeout, ) - response = await self._execute_with_retry( - self._compute_api.intersection, multi_terms_request=request - ) - return Term.from_dto(response.data) async def union( self, @@ -627,17 +769,13 @@ async def union( Returns: Term: A term representing strings matched by ANY of the provided terms. """ - request = _build_request( - MultiTermsRequest, - terms=[t.to_dto() for t in terms], - options=self._build_options( - execution_timeout, response_format, deterministic - ), - ) - response = await self._execute_with_retry( - self._compute_api.union, multi_terms_request=request + return await self._run_nary( + self._compute_api.union, + terms, + response_format, + deterministic, + execution_timeout, ) - return Term.from_dto(response.data) async def difference( self, @@ -778,6 +916,13 @@ async def generate_strings( limit: int, offset: int, execution_timeout: Optional[int] = None, + *, + path_order: Optional[Union[PathOrder, str]] = None, + character_order: Optional[Union[CharacterOrder, str]] = None, + seed: Optional[int] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + charset: Optional[str] = None, ) -> List[str]: """Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. @@ -786,18 +931,42 @@ async def generate_strings( limit: The maximum number of unique strings to return. offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. + path_order: Order in which the paths (shapes) of the language are + scheduled (sweep, interleave or shuffled). Defaults to sweep. + character_order: Order in which the strings within each path are + produced (ascending or shuffled). Defaults to ascending. + seed: Seed behind the shuffled modes. The default seed is fixed, + so two calls sharing a seed generate the same strings and + `offset` pages through them consistently. + min_length: Shortest string to generate. Shorter strings are left + out of the enumeration entirely, `offset` never counting them. + max_length: Longest string to generate. + charset: Restricts generation to the given characters, e.g. + `[a-z]`. Paths requiring a character outside it are dropped. Returns: List[str]: A list of strings that match the term. """ - - request = _build_request( - GenerateStringsRequest, + kwargs = dict( term=term.to_dto(), limit=limit, offset=offset, options=self._build_options(execution_timeout), ) + if path_order is not None: + kwargs["path_order"] = str(path_order) + if character_order is not None: + kwargs["character_order"] = str(character_order) + if seed is not None: + kwargs["seed"] = seed + if min_length is not None: + kwargs["min_length"] = min_length + if max_length is not None: + kwargs["max_length"] = max_length + if charset is not None: + kwargs["charset"] = charset + + request = _build_request(GenerateStringsRequest, **kwargs) response = await self._execute_with_retry( self._generate_api.strings, generate_strings_request=request ) diff --git a/regexsolver/clients/rate_limiter.py b/regexsolver/clients/rate_limiter.py index 3fb4c2a..ee33078 100644 --- a/regexsolver/clients/rate_limiter.py +++ b/regexsolver/clients/rate_limiter.py @@ -1,96 +1,52 @@ import asyncio import logging import threading -from typing import Dict, Optional +import time +from typing import Dict logger = logging.getLogger(__name__) class RateLimiter: - """Shared across all client instances with the same API token and event loop. - - Asyncio primitives (Event, Lock) are NOT thread-safe and are bound to the loop - that created them. This class lazily initializes these primitives to ensure - they are bound to the correct loop. + """Shared across all client instances with the same API token. + + Holds a single deadline on the monotonic clock. `trigger` keeps the later + of the current and the new deadline, so a longer `Retry-After` arriving + while the limiter is already engaged is never dropped. `wait` sleeps until + the deadline and re-checks it after every wake, so a deadline extended by + a concurrent 429 is honored too. Being a plain timestamp, the limiter is + not bound to any event loop and is safe to share across loops. """ def __init__(self): - self._event: Optional[asyncio.Event] = None - self._lock: Optional[asyncio.Lock] = None - self._reopen_task: Optional[asyncio.Task] = None - - def _ensure_primitives(self): - """Lazily initialize asyncio primitives on the current running loop.""" - if self._event is None: - self._event = asyncio.Event() - self._event.set() - if self._lock is None: - self._lock = asyncio.Lock() + self._deadline = 0.0 - async def wait(self): - """Asynchronously waits until the rate limit is no longer triggered. + def trigger(self, retry_after: float) -> None: + """Blocks operations for `retry_after` seconds from now. - If the limiter is currently triggered (e.g., after a 429 error), this method - will block until the delay has passed. + Keeps the later deadline when one is already pending. """ - self._ensure_primitives() - if self._event is None: - raise RuntimeError("RateLimiter event not initialized.") - await self._event.wait() - - async def trigger(self, retry_after: float): - """Triggers the rate limiter for a specific duration. - - Args: - retry_after: The duration in seconds to keep the limiter triggered. - - This method will cause all subsequent calls to wait() to block until - the duration has elapsed. - """ - self._ensure_primitives() - if self._lock is None or self._event is None: - raise RuntimeError("RateLimiter primitives not initialized.") - async with self._lock: - if not self._event.is_set(): - return # already being handled + deadline = time.monotonic() + retry_after + if deadline > self._deadline: logger.debug( f"Rate limit triggered. Delaying operations for {retry_after} seconds." ) - self._event.clear() - if self._reopen_task and not self._reopen_task.done(): - self._reopen_task.cancel() - self._reopen_task = asyncio.create_task(self._lift(retry_after)) - - async def _lift(self, delay: float): - """Background task that lifts the rate limit after the specified delay. + self._deadline = deadline - Args: - delay: The delay in seconds. - """ - await asyncio.sleep(delay) - if self._event is None: - raise RuntimeError("RateLimiter event not initialized.") - logger.debug("Rate limit lifted. Resuming operations.") - self._event.set() + async def wait(self) -> None: + """Asynchronously waits until the rate limit is no longer triggered.""" + while (remaining := self._deadline - time.monotonic()) > 0: + await asyncio.sleep(remaining) -_rate_limiters: Dict[tuple, RateLimiter] = {} +_rate_limiters: Dict[str, RateLimiter] = {} _registry_lock = threading.Lock() def get_rate_limiter(api_token: str) -> RateLimiter: - """Returns a RateLimiter instance for the given token and current event loop.""" - try: - loop = asyncio.get_running_loop() - except RuntimeError: - # If no loop is running, we can't reliably provide a loop-bound limiter. - # This shouldn't happen during normal client usage as methods are called - # within a loop. - loop = None - - key = (api_token, loop) + """Returns the RateLimiter shared by every client using the given token.""" with _registry_lock: - if key not in _rate_limiters: - logger.debug("Creating new RateLimiter instance for current event loop.") - _rate_limiters[key] = RateLimiter() - return _rate_limiters[key] + if api_token not in _rate_limiters: + logger.debug("Creating new RateLimiter instance.") + _rate_limiters[api_token] = RateLimiter() + return _rate_limiters[api_token] diff --git a/regexsolver/clients/synchronous.py b/regexsolver/clients/synchronous.py index be2a346..8ab095c 100644 --- a/regexsolver/clients/synchronous.py +++ b/regexsolver/clients/synchronous.py @@ -5,7 +5,9 @@ from typing import List, Optional, Union from regexsolver.clients.asynchronous import AsyncRegexSolverClient +from regexsolver.models.account_limits import AccountLimits from regexsolver.models.cardinality import Cardinality +from regexsolver.models.generate_order import CharacterOrder, PathOrder from regexsolver.models.length import Length from regexsolver.models.response_format import ResponseFormat from regexsolver.models.term import Term @@ -45,10 +47,18 @@ class RegexSolverClient: While it supports manual `.close()`, it is best used as a context manager. """ - def __init__(self, api_token: str, base_url="https://api.regexsolver.com/v1"): + def __init__( + self, + api_token: str, + base_url: str = "https://api.regexsolver.com/v1", + auto_batch: bool = True, + max_terms_per_request: Optional[int] = None, + ): logger.debug("Initializing RegexSolverClient.") self._loop = _get_or_create_shared_loop() - self._aio = AsyncRegexSolverClient(api_token, base_url) + self._aio = AsyncRegexSolverClient( + api_token, base_url, auto_batch, max_terms_per_request + ) # Ensure the async client is closed even if the user forgets to call close() or use 'with' self._finalizer = weakref.finalize( @@ -86,6 +96,19 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() + # --- ACCOUNT --- + def get_account_limits(self) -> AccountLimits: + """Fetches the plan limits applying to the account. + + The call never consumes request quota (it is only rate-limited) and + the result is cached on the client, so calling it again is free. The + cached `max_terms_count` also drives auto-batching. + + Returns: + AccountLimits: The five plan limits. + """ + return self._run_sync(self._aio.get_account_limits()) + # --- ANALYZE --- def get_cardinality( self, term: Term, execution_timeout: Optional[int] = None @@ -439,6 +462,13 @@ def generate_strings( limit: int, offset: int, execution_timeout: Optional[int] = None, + *, + path_order: Optional[Union[PathOrder, str]] = None, + character_order: Optional[Union[CharacterOrder, str]] = None, + seed: Optional[int] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + charset: Optional[str] = None, ) -> List[str]: """Generates up to `limit` distinct strings matched by `term`, skipping the first `offset` strings. @@ -447,10 +477,33 @@ def generate_strings( limit: The maximum number of unique strings to return. offset: Number of matched strings to skip before starting to collect the results. Used for pagination. execution_timeout: Timeout in milliseconds for the operation. + path_order: Order in which the paths (shapes) of the language are + scheduled (sweep, interleave or shuffled). Defaults to sweep. + character_order: Order in which the strings within each path are + produced (ascending or shuffled). Defaults to ascending. + seed: Seed behind the shuffled modes. The default seed is fixed, + so two calls sharing a seed generate the same strings and + `offset` pages through them consistently. + min_length: Shortest string to generate. Shorter strings are left + out of the enumeration entirely, `offset` never counting them. + max_length: Longest string to generate. + charset: Restricts generation to the given characters, e.g. + `[a-z]`. Paths requiring a character outside it are dropped. Returns: List[str]: A list of strings that match the term. """ return self._run_sync( - self._aio.generate_strings(term, limit, offset, execution_timeout) + self._aio.generate_strings( + term, + limit, + offset, + execution_timeout, + path_order=path_order, + character_order=character_order, + seed=seed, + min_length=min_length, + max_length=max_length, + charset=charset, + ) ) diff --git a/regexsolver/models/account_limits.py b/regexsolver/models/account_limits.py new file mode 100644 index 0000000..c8ce771 --- /dev/null +++ b/regexsolver/models/account_limits.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass + +from regexsolver._generated.models import AccountLimits as GeneratedAccountLimits + + +@dataclass(frozen=True) +class AccountLimits: + """The plan limits currently applying to the account. + + Attributes: + max_requests_count (int): Maximum number of requests allowed per billing period. + max_requests_rate (int): Maximum number of requests allowed per second. 0 means no rate limit is enforced. + max_terms_count (int): Maximum number of terms accepted in a single request. + max_timeout (int): Maximum execution timeout per request, in milliseconds. + max_states_count (int): Maximum number of automaton states an operation may build. + """ + + max_requests_count: int + max_requests_rate: int + max_terms_count: int + max_timeout: int + max_states_count: int + + @classmethod + def from_dto(cls, dto: GeneratedAccountLimits) -> "AccountLimits": + """Converts a generated API model into a high-level AccountLimits object. + + Args: + dto (GeneratedAccountLimits): The raw model from the generated API. + + Returns: + AccountLimits: A high-level instance carrying the five plan limits. + """ + return cls( + max_requests_count=dto.max_requests_count, + max_requests_rate=dto.max_requests_rate, + max_terms_count=dto.max_terms_count, + max_timeout=dto.max_timeout, + max_states_count=dto.max_states_count, + ) + + def __repr__(self) -> str: + return ( + f"" + ) diff --git a/regexsolver/models/generate_order.py b/regexsolver/models/generate_order.py new file mode 100644 index 0000000..e2fdab7 --- /dev/null +++ b/regexsolver/models/generate_order.py @@ -0,0 +1,43 @@ +from enum import Enum + + +class PathOrder(str, Enum): + """Order in which the paths of the language are scheduled when generating + strings — the *shapes* the term allows, as opposed to the characters + filling them. + + Attributes: + SWEEP: Expand one path in full, shortest first, before moving to the + next one. The cheapest way to page through a whole language. + INTERLEAVE: Cover every path once before any path yields a second + string. Best suited to deriving test cases. + SHUFFLED: Interleave with same-length paths visited in an order drawn + from the seed. + """ + + SWEEP = "sweep" + INTERLEAVE = "interleave" + SHUFFLED = "shuffled" + + def __str__(self) -> str: + return str(self.value) + + +class CharacterOrder(str, Enum): + """Order in which the strings within each path are produced when + generating strings. Orthogonal to PathOrder: it does not change *what* can + be generated, only which strings are reached first. + + Attributes: + ASCENDING: Expand each position from the low end of its character + range first — a stable order returning the smallest witnesses of a + path first. + SHUFFLED: A permutation drawn from the seed, so the strings look like + real inputs. Random in look only — generation stays reproducible. + """ + + ASCENDING = "ascending" + SHUFFLED = "shuffled" + + def __str__(self) -> str: + return str(self.value) diff --git a/tests/test_async_client.py b/tests/test_async_client.py index ff5b666..c1b3e80 100644 --- a/tests/test_async_client.py +++ b/tests/test_async_client.py @@ -1,3 +1,4 @@ +import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -27,6 +28,7 @@ @pytest.fixture async def async_client(): client = AsyncRegexSolverClient(api_token="test-token") + client._account_api = AsyncMock() client._analyze_api = AsyncMock() client._compute_api = AsyncMock() client._generate_api = AsyncMock() @@ -34,6 +36,33 @@ async def async_client(): await client.aclose() +def _term_response(value: str): + mock_response = MagicMock() + mock_response.data.actual_instance.type = "regex" + mock_response.data.actual_instance.value = value + return mock_response + + +def _limits_response(max_terms: int = 4): + mock_response = MagicMock() + mock_response.data.max_requests_count = 1000 + mock_response.data.max_requests_rate = 10 + mock_response.data.max_terms_count = max_terms + mock_response.data.max_timeout = 60000 + mock_response.data.max_states_count = 8192 + return mock_response + + +def _too_many_terms_error(provided: int, allowed: int) -> ApiException: + error = ApiException(status=400) + error.body = ( + '{"success": false, ' + f'"error": "{provided} terms provided. Maximum allowed is {allowed}.", ' + '"errorCode": "TooManyTerms"}' + ) + return error + + @pytest.mark.asyncio async def test_get_cardinality_integer(async_client): term = Term.regex("abc") @@ -148,6 +177,9 @@ async def test_error_handling_too_many_terms(async_client): error_400.body = ( '{"success": false, "error": "Too many terms", "errorCode": "TooManyTerms"}' ) + # Auto-batching reacts to TooManyTerms by fetching the limits; when the + # call is already within them, the original error is re-raised. + async_client._account_api.limits.return_value = _limits_response(max_terms=4) async_client._compute_api.union.side_effect = error_400 with pytest.raises(TooManyTermsError): await async_client.union(Term.regex("a"), Term.regex("b")) @@ -267,17 +299,94 @@ async def test_retry_on_429(async_client): term = Term.regex("abc") error_429 = ApiException(status=429) - error_429.headers = {"Retry-After": "0.1"} + error_429.headers = {"Retry-After": "0.05"} success_response = MagicMock() success_response.data.value = True async_client._analyze_api.empty.side_effect = [error_429, success_response] - with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep: - result = await async_client.is_empty(term) - assert result is True - mock_sleep.assert_called() + result = await async_client.is_empty(term) + assert result is True + assert async_client._analyze_api.empty.call_count == 2 + + +@pytest.mark.asyncio +async def test_retry_on_429_lowercase_header(async_client): + error_429 = ApiException(status=429) + error_429.headers = {"retry-after": "0.05"} + + success_response = MagicMock() + success_response.data.value = True + + async_client._analyze_api.empty.side_effect = [error_429, success_response] + + assert await async_client.is_empty(Term.regex("abc")) is True + assert async_client._analyze_api.empty.call_count == 2 + + +@pytest.mark.asyncio +async def test_retry_survives_many_consecutive_429s(async_client): + error_429 = ApiException(status=429) + error_429.headers = {"Retry-After": "0.01"} + + success_response = MagicMock() + success_response.data.value = True + + async_client._analyze_api.empty.side_effect = [error_429] * 8 + [success_response] + + with patch( + "regexsolver.clients.asynchronous.random.uniform", return_value=0.0 + ): + assert await async_client.is_empty(Term.regex("abc")) is True + assert async_client._analyze_api.empty.call_count == 9 + + +@pytest.mark.asyncio +async def test_retry_budget_exhausted(): + from regexsolver import TooManyRequestsError + + # A dedicated token: this test runs on a fake clock, which leaves the + # shared per-token limiter with a nonsense deadline afterwards. + client = AsyncRegexSolverClient(api_token="budget-token") + client._analyze_api = AsyncMock() + + error_429 = ApiException(status=429) + error_429.headers = {"Retry-After": "10"} + client._analyze_api.empty.side_effect = error_429 + + fake_now = [0.0] + + async def fake_sleep(seconds): + fake_now[0] += seconds + + with ( + patch("time.monotonic", side_effect=lambda: fake_now[0]), + patch("asyncio.sleep", new=fake_sleep), + patch("regexsolver.clients.asynchronous.random.uniform", return_value=0.0), + ): + with pytest.raises(TooManyRequestsError) as exc_info: + await client.is_empty(Term.regex("abc")) + assert "Max retries exceeded" in str(exc_info.value) + await client.aclose() + + +@pytest.mark.asyncio +async def test_concurrent_429s_never_surface(async_client): + error_429 = ApiException(status=429) + error_429.headers = {"Retry-After": "0.02"} + + success_response = MagicMock() + success_response.data.value = True + + async_client._analyze_api.empty.side_effect = [error_429, error_429] + [ + success_response + ] * 7 + + results = await asyncio.gather( + *(async_client.is_empty(Term.regex("abc")) for _ in range(5)) + ) + assert results == [True] * 5 @pytest.mark.asyncio @@ -402,3 +511,169 @@ async def test_generate_strings(async_client): async_client._generate_api.strings.return_value = mock_response result = await async_client.generate_strings(term, 3, 0) assert result == ["", "a", "aa"] + + request = async_client._generate_api.strings.call_args.kwargs[ + "generate_strings_request" + ] + # Omitted options fall back to the spec defaults baked into the model. + assert request.path_order is None + assert request.character_order is None + assert request.seed == 0 + assert request.min_length == 0 + assert request.max_length == 100 + assert request.charset is None + + +@pytest.mark.asyncio +async def test_generate_strings_with_options(async_client): + from regexsolver import CharacterOrder, PathOrder + + mock_response = MagicMock() + mock_response.data.strings.value = ["xy"] + async_client._generate_api.strings.return_value = mock_response + + result = await async_client.generate_strings( + Term.regex("[a-z]{2}"), + 5, + 0, + path_order=PathOrder.INTERLEAVE, + character_order=CharacterOrder.SHUFFLED, + seed=42, + min_length=1, + max_length=10, + charset="[a-z]", + ) + assert result == ["xy"] + + request = async_client._generate_api.strings.call_args.kwargs[ + "generate_strings_request" + ] + assert request.path_order == "interleave" + assert request.character_order == "shuffled" + assert request.seed == 42 + assert request.min_length == 1 + assert request.max_length == 10 + assert request.charset == "[a-z]" + + +# --- ACCOUNT LIMITS --- +@pytest.mark.asyncio +async def test_get_account_limits_memoized(async_client): + async_client._account_api.limits.return_value = _limits_response(max_terms=4) + + limits = await async_client.get_account_limits() + assert limits.max_requests_count == 1000 + assert limits.max_requests_rate == 10 + assert limits.max_terms_count == 4 + assert limits.max_timeout == 60000 + assert limits.max_states_count == 8192 + + await async_client.get_account_limits() + assert async_client._account_api.limits.call_count == 1 + + +@pytest.mark.asyncio +async def test_get_account_limits_single_flight(async_client): + async_client._account_api.limits.return_value = _limits_response() + + await asyncio.gather( + async_client.get_account_limits(), async_client.get_account_limits() + ) + assert async_client._account_api.limits.call_count == 1 + + +# --- AUTO-BATCHING --- +def _request_values(call): + request = call.kwargs["multi_terms_request"] + return [t.actual_instance.value for t in request.terms] + + +@pytest.mark.asyncio +async def test_proactive_batching_with_override(): + client = AsyncRegexSolverClient(api_token="batch-token", max_terms_per_request=3) + client._account_api = AsyncMock() + client._compute_api = AsyncMock() + client._compute_api.concat.side_effect = [ + _term_response(f"r{i}") for i in range(4) + ] + + terms = [Term.regex(f"t{i}") for i in range(8)] + result = await client.concat(*terms, response_format="regex") + assert result.get_value() == "r3" + + calls = client._compute_api.concat.call_args_list + assert len(calls) == 4 + # Left fold preserves concat order: contiguous chunks, accumulator first. + assert _request_values(calls[0]) == ["t0", "t1", "t2"] + assert _request_values(calls[1]) == ["r0", "t3", "t4"] + assert _request_values(calls[2]) == ["r1", "t5", "t6"] + assert _request_values(calls[3]) == ["r2", "t7"] + # Only the final request carries the caller's response options. + for call in calls[:3]: + assert call.kwargs["multi_terms_request"].options.response is None + final_options = calls[3].kwargs["multi_terms_request"].options + assert final_options.response.format == "regex" + # The limit was known up front, so no limits fetch happened. + client._account_api.limits.assert_not_called() + await client.aclose() + + +@pytest.mark.asyncio +async def test_reactive_batching_fetches_limits(async_client): + async_client._account_api.limits.return_value = _limits_response(max_terms=4) + async_client._compute_api.union.side_effect = [ + _too_many_terms_error(9, 4), + _term_response("r0"), + _term_response("r1"), + _term_response("r2"), + ] + + terms = [Term.regex(f"t{i}") for i in range(9)] + result = await async_client.union(*terms) + assert result.get_value() == "r2" + + calls = async_client._compute_api.union.call_args_list + assert len(calls) == 4 + assert _request_values(calls[0]) == [f"t{i}" for i in range(9)] + assert _request_values(calls[1]) == ["t0", "t1", "t2", "t3"] + assert _request_values(calls[2]) == ["r0", "t4", "t5", "t6"] + assert _request_values(calls[3]) == ["r1", "t7", "t8"] + assert async_client._account_api.limits.call_count == 1 + + +@pytest.mark.asyncio +async def test_batching_opt_out(): + client = AsyncRegexSolverClient(api_token="no-batch-token", auto_batch=False) + client._account_api = AsyncMock() + client._compute_api = AsyncMock() + client._compute_api.union.side_effect = _too_many_terms_error(9, 4) + + terms = [Term.regex(f"t{i}") for i in range(9)] + with pytest.raises(TooManyTermsError): + await client.union(*terms) + client._account_api.limits.assert_not_called() + await client.aclose() + + +@pytest.mark.asyncio +async def test_batching_limits_fetch_failure_rethrows_original(async_client): + async_client._account_api.limits.side_effect = ApiException( + status=500, reason="Internal Server Error" + ) + async_client._compute_api.union.side_effect = _too_many_terms_error(9, 4) + + terms = [Term.regex(f"t{i}") for i in range(9)] + with pytest.raises(TooManyTermsError): + await async_client.union(*terms) + assert async_client._account_api.limits.call_count == 1 + + +# --- CONSTRUCTOR VALIDATION --- +def test_constructor_rejects_empty_token(): + with pytest.raises(ValueError, match="api_token is required"): + AsyncRegexSolverClient(api_token="") + + +def test_constructor_rejects_invalid_max_terms_per_request(): + with pytest.raises(ValueError, match="max_terms_per_request"): + AsyncRegexSolverClient(api_token="test-token", max_terms_per_request=1) diff --git a/tests/test_rate_limiter.py b/tests/test_rate_limiter.py index 8b550c9..03bb935 100644 --- a/tests/test_rate_limiter.py +++ b/tests/test_rate_limiter.py @@ -1,4 +1,6 @@ import asyncio +import threading +import time import pytest @@ -6,86 +8,77 @@ @pytest.mark.asyncio -async def test_rate_limiter_wait(): +async def test_rate_limiter_wait_without_trigger_returns_immediately(): rl = RateLimiter() - # Primitives are None before use - assert rl._event is None - # Initially set after wait ensures it + start = time.monotonic() await rl.wait() - assert rl._event is not None - assert rl._event.is_set() + assert time.monotonic() - start < 0.05 @pytest.mark.asyncio async def test_rate_limiter_trigger(): rl = RateLimiter() - await rl.trigger(0.1) - # trigger ensures primitives - assert rl._event is not None - assert not rl._event.is_set() + rl.trigger(0.1) + start = time.monotonic() + await rl.wait() + assert time.monotonic() - start >= 0.09 + + +@pytest.mark.asyncio +async def test_rate_limiter_trigger_keeps_later_deadline(): + rl = RateLimiter() + + # A shorter Retry-After arriving second must not shrink the deadline. + rl.trigger(0.2) + rl.trigger(0.05) + start = time.monotonic() + await rl.wait() + assert time.monotonic() - start >= 0.15 - await asyncio.sleep(0.15) - assert rl._event.is_set() + # A longer Retry-After arriving second must extend it. + rl.trigger(0.05) + rl.trigger(0.2) + start = time.monotonic() + await rl.wait() + assert time.monotonic() - start >= 0.15 @pytest.mark.asyncio -async def test_rate_limiter_trigger_already_cleared(): +async def test_rate_limiter_deadline_extended_while_waiting(): rl = RateLimiter() - await rl.trigger(0.2) - task1 = rl._reopen_task + rl.trigger(0.1) + + async def extend(): + await asyncio.sleep(0.05) + rl.trigger(0.2) - # Trigger again while still clearing - await rl.trigger(0.1) - # It should not have changed the event or task if handled correctly - # (actually the implementation returns if not set) - assert rl._event is not None - assert not rl._event.is_set() - assert rl._reopen_task == task1 + start = time.monotonic() + await asyncio.gather(rl.wait(), extend()) + # The waiter woke at the original deadline, re-checked, and slept again. + assert time.monotonic() - start >= 0.2 -def test_get_rate_limiter_loop_aware(): - # We can't easily start multiple loops in one sync test easily without some boilerplate, - # but we can verify the singleton logic still works for the same loop. +def test_get_rate_limiter_shared_by_token(): rl1 = get_rate_limiter("token1") rl2 = get_rate_limiter("token1") assert rl1 is rl2 + assert get_rate_limiter("token2") is not rl1 -@pytest.mark.asyncio -async def test_get_rate_limiter_different_loops(): - # In an async test, get_running_loop() works. +def test_get_rate_limiter_shared_across_loops(): + # The limiter holds only a timestamp, so the registry is keyed by token + # alone and the same instance is shared across event loops and threads. rl1 = get_rate_limiter("token1") - - async def other_loop_task(): - new_loop = asyncio.new_event_loop() - try: - # We must run this in the context of the new loop - # But get_rate_limiter uses get_running_loop() - # So we use the new loop to run a call. - def call_in_loop(): - return get_rate_limiter("token1") - - rl2 = new_loop.run_until_complete( - asyncio.to_thread(call_in_loop) - ) # This is getting complicated - # Simpler: just mock the loop or use a separate thread - return rl2 - finally: - new_loop.close() - - # Let's just use a thread to get a different loop context - import threading - - rl2_container = [] + container = [] def thread_target(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - rl2_container.append(get_rate_limiter("token1")) + container.append(get_rate_limiter("token1")) loop.close() t = threading.Thread(target=thread_target) t.start() t.join() - assert rl1 is not rl2_container[0] + assert rl1 is container[0] From 06299eb88a61033e56c581687082d4116ff312cf Mon Sep 17 00:00:00 2001 From: Alexandre van Beurden <1949482+alexvbrdn@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:05:00 +0200 Subject: [PATCH 47/47] Drop support for python 3.9 --- .github/workflows/ci.yml | 2 +- README.md | 2 +- pyproject.toml | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea0eeda..4dd3dee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 79dad81..fbdd91f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ pip install regexsolver ``` -Requirements: **Python >= 3.9** +Requirements: **Python >= 3.10** ## Quick Start diff --git a/pyproject.toml b/pyproject.toml index 2e70a26..c280ea7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ keywords = [ ] readme = "README.md" license = { file = "LICENSE" } -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ "aiohttp >= 3.8.4", @@ -40,10 +40,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", ]