diff --git a/.gitignore b/.gitignore index 15f5b27..1fdaef5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,10 +12,10 @@ rslv/data *.so # Distribution / packaging +dist/ .Python build/ develop-eggs/ -dist/ downloads/ eggs/ .eggs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f2c5892 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.8.14 + hooks: + # Update the uv lockfile + - id: uv-lock + - id: uv-export + args: ["--no-hashes", "--no-dev", "--no-group", "docs", "--output-file=requirements.txt"] diff --git a/README.md b/README.md index 0d91991..420edbd 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ FastAPI implementation of an identifier resolver. -The `rslv` implementation provides a generic identifier resolution service that may be adapted -to support different schemes. The base requirement is that identifiers are of the -form `scheme:content` where `scheme` is a scheme name (e.g. "doi" or "ark") and `content` is the +The `rslv` implementation provides a generic identifier resolution service that may be adapted +to support different schemes. The base requirement is that identifiers are of the +form `scheme:content` where `scheme` is a scheme name (e.g. "doi" or "ark") and `content` is the value of the identifier (e.g. `10.12345/foo` or `99999/fd99`). ## Operation @@ -38,4 +38,4 @@ With `uvicorn` installed, a development instance of `rslv` can be started from t python rslv/app.py ``` -The service may be accessed at http://localhost:8000/ \ No newline at end of file +The service may be accessed at http://localhost:8000/ diff --git a/_docsrc/build.sh b/_docsrc/build.sh index 887ff2b..8925549 100755 --- a/_docsrc/build.sh +++ b/_docsrc/build.sh @@ -57,4 +57,4 @@ codebraid \ --wrap=none \ --output "$dest" \ --overwrite \ - "$src" \ No newline at end of file + "$src" diff --git a/_docsrc/configuration.md b/_docsrc/configuration.md index 0d3f977..f420c98 100644 --- a/_docsrc/configuration.md +++ b/_docsrc/configuration.md @@ -7,7 +7,7 @@ title: Configuring rslv ## Service Setup - `config.py` options -- `logging.conf` +- `logging.conf` ## Resolver Configuration @@ -55,5 +55,3 @@ scheme:prefix/value Target Template The target is specified in the definition as a template with placeholders that are filled by components of the parsed identifier. - - diff --git a/_docsrc/doc_parts/__init__.py b/_docsrc/doc_parts/__init__.py index 9025dcc..e20d7d5 100644 --- a/_docsrc/doc_parts/__init__.py +++ b/_docsrc/doc_parts/__init__.py @@ -108,4 +108,3 @@ def defn_match_table(pids=EXAMPLE_PIDS): } results.append(result) print(markdown_table(results).set_params(row_sep='markdown', quote=False).get_markdown()) - diff --git a/_docsrc/index.md b/_docsrc/index.md index fc57f67..85ba8e9 100644 --- a/_docsrc/index.md +++ b/_docsrc/index.md @@ -4,7 +4,7 @@ title: "`rslv` Generic Resolver Service" `rslv` implements a resolver service. That is, given an identifier string, the service returns information about the identifier or redirects to the known location of the identified resource. -`rslv` is written in Python and requires python version 3.9 or later. `rslv` may be run as a command line application or more typically, as a web service. +`rslv` is written in Python and requires python version 3.9 or later. `rslv` may be run as a command line application or more typically, as a web service. ## Installation @@ -36,7 +36,7 @@ For development purposes, `rslv` may be run from the command line to provide a t python rslv/app.py ``` -A production deployment should use an ASGI server such as [Uvicorn](https://www.uvicorn.org/) or [Nginx Unit](https://unit.nginx.org/). Uivcorn will generally be deployed behind another web server such as Apache or Nginx whereas `Unit` may be deployed as the web server. +A production deployment should use an ASGI server such as [Uvicorn](https://www.uvicorn.org/) or [Nginx Unit](https://unit.nginx.org/). Uivcorn will generally be deployed behind another web server such as Apache or Nginx whereas `Unit` may be deployed as the web server. @@ -45,4 +45,4 @@ Alternatively, `rslv` may be deployed to a cloud provider such as `Vercel` -## \ No newline at end of file +## diff --git a/_docsrc/matching.md b/_docsrc/matching.md index cf9dfbe..579fa34 100644 --- a/_docsrc/matching.md +++ b/_docsrc/matching.md @@ -22,7 +22,7 @@ It does this by splitting the input identifier string into various components an
-**Figure 1.** Overview of process for handling a user supplied identifier string. The string is split into components as a `parsed_pid` instance. That instance is matched against the available definitions. A match provides a `pid_definition` instance which is used with the `If a match is found then the response is a redirect to the registered target or the matched definition metadata. +**Figure 1.** Overview of process for handling a user supplied identifier string. The string is split into components as a `parsed_pid` instance. That instance is matched against the available definitions. A match provides a `pid_definition` instance which is used with the `If a match is found then the response is a redirect to the registered target or the matched definition metadata.
@@ -38,7 +38,7 @@ The provided identifier string is split into several components (Figure 2) by ap 3. Left trim whitespace or any instances of the characters `:`, `/` from the second portion. This portion is the `content`. 4. Split `content` at the first occurrence of the forward slash character ("/"). 5. The first portion is the `prefix` -6. Left trim whitespace pr any instance of the characters `:`, `/` from the second portion. This portion is the `value`. +6. Left trim whitespace pr any instance of the characters `:`, `/` from the second portion. This portion is the `value`.
@@ -50,7 +50,7 @@ identifier = ark:12345/some_value/with?extra=foo | | | scheme | value prefix - + scheme = "ark" content = "12345/some_value/with?extra=foo" prefix = "12345" @@ -59,7 +59,7 @@ value = "some_value/with?extra=foo"
-**Figure 2.** Components of a `parsed_pid`. After parsing, extracted components of the identifier are available for locating a matching definition and formatting the response. +**Figure 2.** Components of a `parsed_pid`. After parsing, extracted components of the identifier are available for locating a matching definition and formatting the response.
@@ -105,7 +105,7 @@ examples = [ "ark:99999/foozle", "ark:example/foozle", "ark:99999/fk4qwerty", - "ark:99999/fkqwerty", + "ark:99999/fkqwerty", ] doc_parts.defn_match_table(pids=examples) ``` diff --git a/docs/matching.md b/docs/matching.md index d7cba20..1644e22 100644 --- a/docs/matching.md +++ b/docs/matching.md @@ -1,6 +1,6 @@ --- comment: | - ’’’ codebraid pandoc –katex –from markdown+tex_math_single_backslash –filter pandoc-sidenote + ’’’ codebraid pandoc –katex –from markdown+tex_math_single_backslash –filter pandoc-sidenote –to html5+smart –template=$HOME/.pandoc/templates/template.html5 \ --css=$HOME/.pandoc/theme.css –toc –wrap=none matching.md \> matching.html ’’’ @@ -49,7 +49,7 @@ The provided identifier string is split into several components (Figure 2) by ap | | | scheme | value prefix - + scheme = "ark" content = "12345/some_value/with?extra=foo" prefix = "12345" diff --git a/pyproject.toml b/pyproject.toml index 8261d3a..7082751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rslv" -version = "0.9.6" +version = "0.10.0" description = "Provides an identifier resolver service in FastAPI." authors = [{ name = "datadavev", email = "605409+datadavev@users.noreply.github.com" }] requires-python = ">=3.9,<3.13" @@ -28,6 +28,7 @@ dev = [ "httpx>=0.28.1,<0.29", "flake8>=7.1.1,<8", "black>=25.1.0,<26", + "pre-commit>=4.3.0", ] cli = [ "click>=8,<9", @@ -47,4 +48,3 @@ default-groups = [ ] [tool.poetry_bumpversion.file."rslv/__init__.py"] - diff --git a/requirements.txt b/requirements.txt index af2f6f0..d2f0935 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv export --no-hashes --no-group dev --no-group docs --format requirements-txt +# uv export --no-hashes --no-dev --no-group docs --output-file=requirements.txt -e . annotated-types==0.7.0 # via pydantic diff --git a/rslv/config.py b/rslv/config.py index 70ec662..f52040d 100644 --- a/rslv/config.py +++ b/rslv/config.py @@ -46,6 +46,8 @@ class Settings(pydantic_settings.BaseSettings): # Note that this should be set False on services offering one-to-one matching of # definitions to PIDs. For N2T and arks.org this sould be true to match legacy behavior. auto_introspection: bool = True + # Optional header that if set, service returns a 200 code instead of redirect. + request_no_redirect: str = "x-no-redirect" def load_settings(): diff --git a/rslv/lib_rslv/__init__.py b/rslv/lib_rslv/__init__.py index 3eeffa9..34e9f03 100644 --- a/rslv/lib_rslv/__init__.py +++ b/rslv/lib_rslv/__init__.py @@ -39,14 +39,14 @@ def split_identifier_string(pid_str: str) -> typing.Dict[str, typing.Any]: parsed["scheme"] = _parts[0].strip().lower() try: parsed["content"] = _parts[1].lstrip(" /:") - parsed["content"] = parsed["content"].strip() # type: ignore + parsed["content"] = parsed["content"].strip() # type: ignore except IndexError: return parsed - _parts = parsed["content"].split("/", 1) # type: ignore + _parts = parsed["content"].split("/", 1) # type: ignore parsed["prefix"] = _parts[0].strip() try: parsed["value"] = _parts[1].lstrip(" /") - parsed["value"] = parsed["value"].strip() # type: ignore + parsed["value"] = parsed["value"].strip() # type: ignore except IndexError: pass return parsed diff --git a/rslv/lib_rslv/piddefine.py b/rslv/lib_rslv/piddefine.py index 8cb7a64..d30b014 100644 --- a/rslv/lib_rslv/piddefine.py +++ b/rslv/lib_rslv/piddefine.py @@ -442,12 +442,18 @@ def parse( ) parts["suffix"] = pid_str[suffix_pos:] - # Hack alert - need to deal with the oddness of ARK identifiers ignoring hyphens. + # Hack alert - Optionally need to deal with the oddness of ARK identifiers ignoring hyphens. if parts["scheme"] == "ark": - # remove hyphens from the content and value portions, but not from the query portion, if present... - parts["content"] = rslv.lib_rslv.remove_hyphens(parts["content"]) - parts["value"] = rslv.lib_rslv.remove_hyphens(parts["value"]) - parts["suffix"] = rslv.lib_rslv.remove_hyphens(parts["suffix"]) + # Hyphen stripping for ARKs is optionally set at the definition level + # and defaults to True to match the legacy resolver behavior + strip_ark_hyphens = True + if pid_definition.properties is not None: + strip_ark_hyphens = pid_definition.properties.get("strip_hyphens", True) + if strip_ark_hyphens: + # remove hyphens from the content and value portions, but not from the query portion, if present... + parts["content"] = rslv.lib_rslv.remove_hyphens(parts["content"]) + parts["value"] = rslv.lib_rslv.remove_hyphens(parts["value"]) + parts["suffix"] = rslv.lib_rslv.remove_hyphens(parts["suffix"]) return parts, pid_definition def list_schemes(self, valid_targets_only: bool = False): diff --git a/rslv/routers/resolver.py b/rslv/routers/resolver.py index 82b8315..a403561 100644 --- a/rslv/routers/resolver.py +++ b/rslv/routers/resolver.py @@ -122,12 +122,16 @@ def get_service_info(request: fastapi.Request, valid: bool = True): def handle_get_info( - request: fastapi.Request, cleaned_identifier: CleanedIdentifierRequest + request: fastapi.Request, + cleaned_identifier: CleanedIdentifierRequest, + pid_config, + pid_parts: dict, + definition: typing.Optional[rslv.lib_rslv.piddefine.PidDefinition] ): - pid_config = rslv.lib_rslv.piddefine.PidDefinitionCatalog(request.state.dbsession) - pid_parts, definition = pid_config.parse( - cleaned_identifier.cleaned, resolve_synonym=False - ) + #pid_config = rslv.lib_rslv.piddefine.PidDefinitionCatalog(request.state.dbsession) + #pid_parts, definition = pid_config.parse( + # cleaned_identifier.cleaned, resolve_synonym=False + #) # TODO: This is where a definition specific handler can be used for # further processing of the PID, e.g. to remove hyphens from an ark. # Basically, add a property to the definition that contains the name @@ -216,7 +220,18 @@ def get_info( str(request.url), identifier, request.app.state.settings.service_pattern ) - return handle_get_info(request, cleaned_identifier) + pid_config = rslv.lib_rslv.piddefine.PidDefinitionCatalog(request.state.dbsession) + pid_parts, definition = pid_config.parse( + cleaned_identifier.cleaned, resolve_synonym=False + ) + + return handle_get_info( + request, + cleaned_identifier, + pid_config, + pid_parts, + definition + ) @router.head( @@ -275,16 +290,22 @@ def get_resolve( str(request.url), identifier, request.app.state.settings.service_pattern ) - # If the request was for introspection (inflection) use the info handler - if cleaned_identifier.is_introspection: - return handle_get_info(request, cleaned_identifier) # Get the identifier configuration catalog pid_config = rslv.lib_rslv.piddefine.PidDefinitionCatalog(request.state.dbsession) # Split the identifier string into components and find the best match from the catalog pid_parts, definition = pid_config.parse(cleaned_identifier.cleaned) - # TODO: see above in get_info for PID handling with specific schemes. + + # If the request was for introspection (inflection) use the info handler + if cleaned_identifier.is_introspection: + return handle_get_info( + request, + cleaned_identifier, + pid_config, + pid_parts, + definition + ) if definition is None: # Return a 404 response and include the pid parts in the body with a @@ -307,7 +328,13 @@ def get_resolve( None, "", ]: - return handle_get_info(request, cleaned_identifier) + return handle_get_info( + request, + cleaned_identifier, + pid_config, + pid_parts, + definition + ) # If the PID value part matches the value part of the matched definition, # then return the definition information. This is sketchy behavior but included # here because it follows the legacy N2T behavior. It can be disabled through @@ -316,11 +343,21 @@ def get_resolve( request.app.state.settings.auto_introspection and pid_parts["value"] == definition.value ): - return handle_get_info(request, cleaned_identifier) + return handle_get_info( + request, + cleaned_identifier, + pid_config, + pid_parts, + definition + ) # OK, past all the edge cases, redirect the client to the registered target. pid_parts["canonical"] = pid_format(pid_parts, definition.canonical) pid_parts["status_code"] = response_status_code headers = {"Location": _target} + # Check if request includes no redirect header and + # override the redirect if so. + if request.app.state.settings.request_no_redirect in request.headers: + response_status_code = 200 return fastapi.responses.JSONResponse( content=pid_parts, headers=headers, diff --git a/rslv/static/style.css b/rslv/static/style.css index 7a06dcd..1cb1983 100644 --- a/rslv/static/style.css +++ b/rslv/static/style.css @@ -193,4 +193,4 @@ ul.blog-posts li a:visited { .helptext { color: #aaa; } -} \ No newline at end of file +} diff --git a/rslv/templates/index.html b/rslv/templates/index.html index acab596..31ad6d3 100644 --- a/rslv/templates/index.html +++ b/rslv/templates/index.html @@ -34,4 +34,4 @@

rslv - Identifier Resolution Service

- \ No newline at end of file + diff --git a/tests/test_parser.py b/tests/test_parser.py index b52098b..f3fb912 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -71,6 +71,13 @@ def do_add(cfg, entry): scheme="ark", prefix="12345", value="up", properties={"name": "Frank"} ), ) + do_add( + cfg, + rslv.lib_rslv.piddefine.PidDefinition( + scheme="ark", prefix="12345", value="nostrip", + properties={"name": "Frank", "strip_hyphens":False} + ), + ) cfg.refresh_metadata() @@ -90,6 +97,10 @@ def do_add(cfg, entry): "bark:99999/fk44wlr;jglerig", {"scheme": "ark", "prefix": "99999", "value": "fk4"}, ), + ( + "ark:12345/nostrip-test", + {"scheme": "ark", "prefix": "12345", "value": "nostrip", "suffix":"-test"}, + ) ) diff --git a/tests/test_service.py b/tests/test_service.py index abeca0c..3fba5b9 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -122,6 +122,16 @@ def do_add(cfg, entry): properties={"tag": 9}, ), ) + do_add( + cfg, + rslv.lib_rslv.piddefine.PidDefinition( + scheme="ark", + prefix="99999", + value="nostrip", + target="http://example.org/ark${suffix}", + properties={"tag": 10, "strip_hyphens": False}, + ), + ) cfg.refresh_metadata() finally: session.close() @@ -184,6 +194,7 @@ def test_info_schemes(test, expected): (["ark:/12345", "GET"], {"target": "https://example.com/ark:/12345", "status": 200}), (["ark:12345", "GET"], {"target": "https://example.com/ark:12345", "status": 200}), (["ark:99999/912345/foo", "GET"], {"target": "http://arks.org/ark:12345/foo", "status": 302, "tag":9}), + (["ark:99999/nostrip/foo-bar", "GET"], {"target": "http://example.org/ark/foo-bar", "status": 302, "tag":10}), ) @pytest.mark.parametrize("test,expected", resolve_cases) @@ -225,3 +236,12 @@ def test_resolve_schemes2(test, expected): finally: session.close() + +@pytest.mark.parametrize("test,expected", resolve_cases) +def test_resolve_schemes_no_redirect(test, expected): + L.info("test_resolve_schemes1: %s", test) + client = fastapi.testclient.TestClient(rslv.app.app, follow_redirects=False) + response = client.request(test[1], f"/{test[0]}", headers={"x-no-redirect":"1"}) + _match = response.json() + L.info(json.dumps(_match, indent=2)) + assert response.status_code == 200 diff --git a/uv.lock b/uv.lock index 60aec43..784aabc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <3.13" [[package]] @@ -78,6 +78,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -172,6 +181,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -198,6 +216,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + [[package]] name = "flake8" version = "7.3.0" @@ -330,6 +357,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "identify" +version = "2.6.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -426,6 +462,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -462,6 +507,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + [[package]] name = "py-markdown-table" version = "1.3.0" @@ -707,7 +768,7 @@ wheels = [ [[package]] name = "rslv" -version = "0.9.6" +version = "0.10.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -725,6 +786,7 @@ dev = [ { name = "black" }, { name = "flake8" }, { name = "httpx" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "requests" }, { name = "uvicorn", extra = ["standard"] }, @@ -752,6 +814,7 @@ dev = [ { name = "black", specifier = ">=25.1.0,<26" }, { name = "flake8", specifier = ">=7.1.1,<8" }, { name = "httpx", specifier = ">=0.28.1,<0.29" }, + { name = "pre-commit", specifier = ">=4.3.0" }, { name = "pytest", specifier = ">=8.3.4,<9" }, { name = "requests", specifier = ">=2.32.3,<3" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<0.35" }, @@ -1001,6 +1064,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696, upload-time = "2024-10-14T23:38:33.633Z" }, ] +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + [[package]] name = "watchfiles" version = "1.1.0"