Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 25 additions & 2 deletions docs/url.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down
23 changes: 21 additions & 2 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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):
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
28 changes: 15 additions & 13 deletions tna_utilities/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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):
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand Down