From 3f38a2a92bb74207e66278dcd6379ca10e5c45f5 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 19 Aug 2026 09:51:30 +0100 Subject: [PATCH] v1.7.0 --- CHANGELOG.md | 11 +++++++---- docs/index.md | 7 ++++++- docs/url.md | 27 +++++++++++++++++++++++++-- pyproject.toml | 2 +- tests/test_url.py | 23 +++++++++++++++++++++-- tna_utilities/url.py | 28 +++++++++++++++------------- 6 files changed, 75 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5147787..c073cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,18 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/nationalarchives/python-utilities/compare/v1.6.0...HEAD) +## [Unreleased](https://github.com/nationalarchives/python-utilities/compare/v1.7.0...HEAD) ### Added ### Changed - -- `QueryStringTransformer` now has a `tolerant` option which doesn't raise exceptions for missing keys - ### Deprecated ### Removed ### Fixed ### Security +## [1.7.0](https://github.com/nationalarchives/python-utilities/compare/v1.6.0...v1.7.0) - 2026-08-19 + +### Added + +- `QueryStringTransformer` now has a `tolerant` option which doesn't raise exceptions for missing keys + ## [1.6.0](https://github.com/nationalarchives/python-utilities/compare/v1.5.0...v1.6.0) - 2026-08-04 ### Added diff --git a/docs/index.md b/docs/index.md index dbd1a83..1fae51c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,14 @@ # TNA Python Utilities +This is a library of common Python functions, some specific to The National Archives to help speed up some aspects of Python application development. + +- [API](./api.md) +- [Components](./component.md) - [Currency](./currency.md) - [Dates and times](./dates-and-times.md) +- [Numbers](./number.md) - [Security](./security.md) -- [String](./string.md) +- [Strings](./string.md) - [URLs](./url.md) ## Optional modules diff --git a/docs/url.md b/docs/url.md index 950bcc8..9634347 100644 --- a/docs/url.md +++ b/docs/url.md @@ -4,6 +4,10 @@ A utility class to manipulate query strings. +Use this to take a query string like `?q=pizza&page=3&category=social` and manipulate only what you need, for example changing the `page` parameter to `4`, or switching out the category from `social` to `work`, while keeping the rest of the query string intact. + +This can be useful when generating things like links in filters, avoiding the need to `POST` a form and have a stateful page that can't be shared or refreshed. + ### Instantiation #### Flask @@ -113,11 +117,30 @@ print(qs.get_query_string()) # ?a=4&b=2 # Chainable (as of v1.1.0) -print(qs.add_parameter_value( +new_query_string = qs.add_parameter_value( "a", "4" ).toggle_parameter_value( "b", "3" ).remove_parameter_value( "a", "1" -).get_query_string()) +).get_query_string() +``` + +### Tolerant mode + +> Added in `v1.7.0`. + +```python +from tna_utilities.url import QueryStringTransformer + +# ?a=1 +qs = QueryStringTransformer([("a", ["1"])]) +qs.remove_parameter_value("b", "2") # Raises KeyError: Parameter 'b' does not exist +qs.is_value_in_parameter("c", "3") # Raises KeyError: Parameter 'c' does not exist + +# ?a=1 +qs_tolerant = QueryStringTransformer([("a", ["1"])], tolerant=True) +qs_tolerant.remove_parameter_value("b", "2") # No exception raised +print(qs_tolerant.is_value_in_parameter("c", "3")) +# False ``` diff --git a/pyproject.toml b/pyproject.toml index 73f067d..fe9dc24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tna-utilities" -version = "1.6.0" +version = "1.7.0" requires-python = ">=3.10" authors = [ {name = "Andrew Hosgood", email = "andrew.hosgood@nationalarchives.gov.uk"}, diff --git a/tests/test_url.py b/tests/test_url.py index 23fdea2..cf64303 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -72,6 +72,9 @@ def test_add_parameter(self): self.assertTrue(manipulator.parameter_exists("h")) self.assertEqual(manipulator.parameter_values("h"), ["False"]) + with self.assertRaises(ValueError): + manipulator.add_parameter("h", [True]) + self.assertEqual( manipulator.get_query_string(), "?a=1&b=2&b=3&e=&f=4&g=5&g=6&h=False" ) @@ -93,7 +96,7 @@ def test_remove_parameter(self): self.assertEqual(manipulator.remove_parameter("b"), manipulator) self.assertFalse(manipulator.parameter_exists("b")) with self.assertRaises(KeyError): - self.assertEqual(manipulator.remove_parameter("c"), manipulator) + manipulator.remove_parameter("c") self.assertEqual(manipulator.get_query_string(), "") def test_is_value_in_parameter(self): @@ -103,7 +106,7 @@ def test_is_value_in_parameter(self): self.assertTrue(manipulator.is_value_in_parameter("b", "3")) self.assertFalse(manipulator.is_value_in_parameter("b", "4")) with self.assertRaises(KeyError): - self.assertFalse(manipulator.is_value_in_parameter("c", "5")) + manipulator.is_value_in_parameter("c", "5") def test_toggle_parameter_value(self): manipulator = QueryStringTransformer(self.test_query) @@ -142,6 +145,19 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.test_query = [("a", ["1"]), ("b", ["2", "3"])] + def test_add_parameter(self): + manipulator = QueryStringTransformer(self.test_query, tolerant=True) + + self.assertEqual(manipulator.add_parameter("h", [False]), manipulator) + self.assertTrue(manipulator.parameter_exists("h")) + self.assertEqual(manipulator.parameter_values("h"), ["False"]) + + self.assertEqual(manipulator.add_parameter("h", [True]), manipulator) + self.assertTrue(manipulator.parameter_exists("h")) + self.assertEqual(manipulator.parameter_values("h"), ["True"]) + + self.assertEqual(manipulator.get_query_string(), "?a=1&b=2&b=3&h=True") + def test_update_parameter(self): manipulator = QueryStringTransformer(self.test_query, tolerant=True) self.assertEqual(manipulator.update_parameter("a", "10"), manipulator) @@ -151,6 +167,9 @@ def test_update_parameter(self): self.assertEqual(manipulator.update_parameter("c", ["40"]), manipulator) self.assertEqual(manipulator.parameter_values("c"), ["40"]) self.assertEqual(manipulator.get_query_string(), "?a=10&b=20&b=30&c=40") + self.assertEqual(manipulator.update_parameter("c", "50"), manipulator) + self.assertEqual(manipulator.parameter_values("c"), ["50"]) + self.assertEqual(manipulator.get_query_string(), "?a=10&b=20&b=30&c=50") def test_remove_parameter(self): manipulator = QueryStringTransformer(self.test_query, tolerant=True) diff --git a/tna_utilities/url.py b/tna_utilities/url.py index e0f8eb8..1496af6 100644 --- a/tna_utilities/url.py +++ b/tna_utilities/url.py @@ -9,7 +9,8 @@ class QueryStringTransformer: args: An object representing the query parameters, typically an ImmutableMultiDict (Django) or QueryDict (Flask) which can be accessed with request.GET (Django) or request.args (Flask). - tolerant: If True, the transformer will not raise exceptions for certain operations. + tolerant: If True, the transformer will not raise exceptions when + keys don't exist. """ def __init__(self, args=None, tolerant=False) -> None: @@ -52,19 +53,20 @@ def add_parameter( ) -> "QueryStringTransformer": """ Add a new parameter to the query parameters. - Raises a ValueError if the parameter already exists. + Raises a ValueError if the parameter already exists and tolerant mode is not enabled. """ - for key, _vals in self.args: - if key == parameter: - if self.tolerant: - return self - raise ValueError(f"Parameter '{parameter}' already exists") + parameter_exists = self.parameter_exists(parameter) + if parameter_exists and not self.tolerant: + raise ValueError(f"Parameter '{parameter}' already exists") if not isinstance(values, list): values = [str(values)] if values is not None else [] else: values = [str(v) for v in values] - self.args.append((parameter, values)) + if parameter_exists: + self.update_parameter(parameter, values) + else: + self.args.append((parameter, values)) return self def update_parameter( @@ -83,7 +85,7 @@ def update_parameter( def remove_parameter(self, parameter: str) -> "QueryStringTransformer": """ Remove a parameter from the query parameters. - Raises a KeyError if the parameter does not exist. + Raises a KeyError if the parameter does not exist and tolerant mode is not enabled. """ for index, (key, _vals) in enumerate(self.args): @@ -97,7 +99,7 @@ def remove_parameter(self, parameter: str) -> "QueryStringTransformer": def is_value_in_parameter(self, parameter: str, value: str | int) -> bool: """ Check if a specific value exists within a parameter's values. - Raises a KeyError if the parameter does not exist. + Raises a KeyError if the parameter does not exist and tolerant mode is not enabled. """ for key, values in self.args: @@ -112,7 +114,7 @@ def add_parameter_value( ) -> "QueryStringTransformer": """ Add a specific value to a parameter's values. - Raises a KeyError if the parameter does not exist. + Raises a KeyError if the parameter does not exist and tolerant mode is not enabled. """ for key, values in self.args: @@ -131,7 +133,7 @@ def toggle_parameter_value( """ Toggle a value within a parameter's values. If the value exists, it will be removed; if it does not exist, it will be added. - Raises a KeyError if the parameter does not exist. + Raises a KeyError if the parameter does not exist and tolerant mode is not enabled. """ for key, values in self.args: @@ -152,7 +154,7 @@ def remove_parameter_value( ) -> "QueryStringTransformer": """ Remove a specific value from a parameter's values. - Raises a KeyError if the parameter does not exist or if the value is not present. + Raises a KeyError if the parameter does not exist or if the value is not present and tolerant mode is not enabled. """ for key, values in self.args: