|
2 | 2 | import re |
3 | 3 | import unicodedata |
4 | 4 | from datetime import datetime, timezone |
| 5 | +from urllib.parse import urlencode |
5 | 6 |
|
6 | | -__author__ = 'Jon Nappi' |
7 | | -__all__ = ['slugify', 'airs_date', 'now', 'timestamp', 'extract_ids'] |
| 7 | +__author__ = 'Jon Nappi, Elan Ruusamäe' |
| 8 | +__all__ = ['slugify', 'airs_date', 'now', 'timestamp', 'extract_ids', |
| 9 | + 'build_uri'] |
8 | 10 |
|
9 | 11 |
|
10 | 12 | def slugify(value): |
@@ -56,3 +58,54 @@ def extract_ids(id_dict): |
56 | 58 | """ |
57 | 59 | id_dict.update(id_dict.pop('ids', {})) |
58 | 60 | return id_dict |
| 61 | + |
| 62 | + |
| 63 | +def _validate_pagination_param(name, value): |
| 64 | + """Validate and coerce a pagination parameter to a positive integer. |
| 65 | +
|
| 66 | + :param name: Parameter name used in error messages. |
| 67 | + :param value: Value to validate. |
| 68 | + :return: The validated integer value. |
| 69 | + :raises ValueError: If value is not a valid positive integer. |
| 70 | + """ |
| 71 | + |
| 72 | + try: |
| 73 | + # bool is a subclass of int; reject it explicitly to avoid accepting True/False. |
| 74 | + if isinstance(value, bool): |
| 75 | + raise ValueError |
| 76 | + |
| 77 | + # Avoid silently truncating noninteger floats (e.g., 1.9 -> 1). |
| 78 | + if isinstance(value, float) and not value.is_integer(): |
| 79 | + raise ValueError |
| 80 | + |
| 81 | + value = int(value) |
| 82 | + except (TypeError, ValueError): |
| 83 | + raise ValueError(f'{name} must be a valid integer') |
| 84 | + |
| 85 | + if value < 1: |
| 86 | + raise ValueError(f'{name} must be a positive integer') |
| 87 | + |
| 88 | + return value |
| 89 | + |
| 90 | + |
| 91 | +def build_uri(uri, page=None, limit=None, **params): |
| 92 | + """Format *uri* and append pagination query parameters. |
| 93 | +
|
| 94 | + ``page`` and ``limit`` are validated as positive integers and become query |
| 95 | + parameters. Remaining keyword arguments are applied via ``str.format`` on |
| 96 | + *uri*; any ``None`` values are dropped before formatting (so a missing |
| 97 | + placeholder will raise ``KeyError``). |
| 98 | + """ |
| 99 | + params = {key: value for key, value in params.items() if value is not None} |
| 100 | + uri = uri.format(**params) |
| 101 | + |
| 102 | + query = [] |
| 103 | + if page is not None: |
| 104 | + query.append(('page', _validate_pagination_param('page', page))) |
| 105 | + if limit is not None: |
| 106 | + query.append(('limit', _validate_pagination_param('limit', limit))) |
| 107 | + |
| 108 | + if query: |
| 109 | + uri += ('&' if '?' in uri else '?') + urlencode(query) |
| 110 | + |
| 111 | + return uri |
0 commit comments