ulid-transform 2.2.4__tar.gz → 2.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ulid-transform
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: Create and transform ULIDs
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -68,7 +68,7 @@ This library will use the C++ implementation from https://github.com/suyash/ulid
68
68
  '000000016JC62D620DGYNG2R8H'
69
69
  >>> ulid_transform.ulid_to_bytes('0001HZX0NW00GW0X476W5TVBFE')
70
70
  b'\x00\x00c\xfe\x82\xbc\x00!\xc0t\x877\x0b\xad\xad\xee'
71
- >> ulid_transform.bytes_to_ulid(b"\x01\x86\x99?\xe8\xf3\x11\xbc\xed\xef\x86U.9\x03z")
71
+ >>> ulid_transform.bytes_to_ulid(b"\x01\x86\x99?\xe8\xf3\x11\xbc\xed\xef\x86U.9\x03z")
72
72
  '01GTCKZT7K26YEVVW6AMQ3J0VT'
73
73
  >>> ulid_transform.ulid_to_bytes_or_none('0001HZX0NW00GW0X476W5TVBFE')
74
74
  b'\x00\x00c\xfe\x82\xbc\x00!\xc0t\x877\x0b\xad\xad\xee'
@@ -44,7 +44,7 @@ This library will use the C++ implementation from https://github.com/suyash/ulid
44
44
  '000000016JC62D620DGYNG2R8H'
45
45
  >>> ulid_transform.ulid_to_bytes('0001HZX0NW00GW0X476W5TVBFE')
46
46
  b'\x00\x00c\xfe\x82\xbc\x00!\xc0t\x877\x0b\xad\xad\xee'
47
- >> ulid_transform.bytes_to_ulid(b"\x01\x86\x99?\xe8\xf3\x11\xbc\xed\xef\x86U.9\x03z")
47
+ >>> ulid_transform.bytes_to_ulid(b"\x01\x86\x99?\xe8\xf3\x11\xbc\xed\xef\x86U.9\x03z")
48
48
  '01GTCKZT7K26YEVVW6AMQ3J0VT'
49
49
  >>> ulid_transform.ulid_to_bytes_or_none('0001HZX0NW00GW0X476W5TVBFE')
50
50
  b'\x00\x00c\xfe\x82\xbc\x00!\xc0t\x877\x0b\xad\xad\xee'
@@ -1,9 +1,9 @@
1
1
  """Build optional C extension modules."""
2
2
 
3
+ from distutils.command.build_ext import build_ext
3
4
  import logging
4
5
  import os
5
- from distutils.command.build_ext import build_ext
6
- from os.path import join
6
+ from pathlib import Path
7
7
  from typing import Any
8
8
 
9
9
  try:
@@ -11,6 +11,8 @@ try:
11
11
  except ImportError:
12
12
  from distutils.core import Extension
13
13
 
14
+ _LOGGER = logging.getLogger(__name__)
15
+
14
16
 
15
17
  def getenv_bool(key: str, default: bool = False) -> bool:
16
18
  value = os.environ.get(key, str(default)).lower()
@@ -18,13 +20,14 @@ def getenv_bool(key: str, default: bool = False) -> bool:
18
20
  return True
19
21
  if value in ("0", "false", "no"):
20
22
  return False
21
- raise ValueError(f"Invalid value for boolean envvar {key}: {value}")
23
+ msg = f"Invalid value for boolean envvar {key}: {value}"
24
+ raise ValueError(msg)
22
25
 
23
26
 
24
27
  ulid_module = Extension(
25
28
  "ulid_transform._ulid_impl",
26
29
  [
27
- join("src", "ulid_transform", "_ulid_impl.cpp"),
30
+ str(Path("src") / "ulid_transform" / "_ulid_impl.cpp"),
28
31
  ],
29
32
  language="c++",
30
33
  extra_compile_args=["-std=c++11", "-O3", "-g0"],
@@ -39,7 +42,7 @@ class BuildExt(build_ext):
39
42
  try:
40
43
  super().build_extensions()
41
44
  except Exception: # nosec
42
- logging.exception("Failed to build extensions")
45
+ _LOGGER.exception("Failed to build extensions")
43
46
  if getenv_bool("REQUIRE_CYTHON") or getenv_bool("REQUIRE_EXTENSION"):
44
47
  raise
45
48
 
@@ -55,6 +58,6 @@ def build(setup_kwargs: Any) -> None:
55
58
  }
56
59
  )
57
60
  except Exception:
58
- logging.exception("Failed to configure C extension")
61
+ _LOGGER.exception("Failed to configure C extension")
59
62
  if getenv_bool("REQUIRE_CYTHON") or getenv_bool("REQUIRE_EXTENSION"):
60
63
  raise
@@ -0,0 +1,173 @@
1
+ [project]
2
+ name = "ulid-transform"
3
+ version = "2.2.5"
4
+ license = "MIT"
5
+ description = "Create and transform ULIDs"
6
+ readme = "README.md"
7
+ authors = [{ name = "J. Nick Koston", email = "nick@koston.org" }]
8
+ requires-python = ">=3.11"
9
+
10
+ [project.urls]
11
+ "Repository" = "https://github.com/bluetooth-devices/ulid-transform"
12
+ "Bug Tracker" = "https://github.com/bluetooth-devices/ulid-transform/issues"
13
+ "Changelog" = "https://github.com/bluetooth-devices/ulid-transform/blob/main/CHANGELOG.md"
14
+
15
+ [tool.poetry]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Natural Language :: English",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+ packages = [
24
+ { include = "ulid_transform", from = "src" },
25
+ ]
26
+
27
+ [tool.poetry.build]
28
+ generate-setup-file = true
29
+ script = "build_ext.py"
30
+
31
+ [tool.poetry.dependencies]
32
+ python = "^3.11"
33
+
34
+ [tool.poetry.group.dev.dependencies]
35
+ pytest = ">=9.0.3,<10"
36
+ pytest-cov = ">=3,<8"
37
+ setuptools = ">=65.4.1,<83.0.0"
38
+ pytest-codspeed = ">=5.0.2,<6.0.0"
39
+
40
+
41
+ [tool.poetry.group.benchmark.dependencies]
42
+ ulid-py = "^1.1.0"
43
+ ulid2 = "^0.3.0"
44
+ pytest-benchmark = ">=4,<6"
45
+
46
+ [tool.semantic_release]
47
+ version_toml = ["pyproject.toml:project.version"]
48
+ version_variables = [
49
+ "src/ulid_transform/__init__.py:__version__",
50
+ ]
51
+ build_command = "pip install poetry && poetry build"
52
+
53
+ [tool.pytest.ini_options]
54
+ pythonpath = ["src"]
55
+
56
+ [tool.coverage.run]
57
+ branch = true
58
+
59
+ [tool.coverage.report]
60
+ exclude_lines = [
61
+ "pragma: no cover",
62
+ "@overload",
63
+ "if TYPE_CHECKING",
64
+ "raise NotImplementedError",
65
+ 'if __name__ == "__main__":',
66
+ ]
67
+
68
+ [tool.mypy]
69
+ check_untyped_defs = true
70
+ disallow_any_generics = true
71
+ disallow_incomplete_defs = true
72
+ disallow_untyped_defs = true
73
+ mypy_path = "src/"
74
+ no_implicit_optional = true
75
+ show_error_codes = true
76
+ warn_unreachable = true
77
+ warn_unused_ignores = true
78
+ exclude = [
79
+ 'setup.py',
80
+ ]
81
+
82
+ [[tool.mypy.overrides]]
83
+ module = "tests.*"
84
+ allow_untyped_defs = true
85
+ warn_unused_ignores = false
86
+
87
+ [[tool.mypy.overrides]]
88
+ module = "bench.*"
89
+ allow_untyped_defs = true
90
+
91
+ [tool.ruff]
92
+ required-version = ">=0.5.0"
93
+ target-version = "py311"
94
+
95
+ [tool.ruff.lint]
96
+ select = [
97
+ "A", # flake8-builtins
98
+ "ARG", # flake8-unused-arguments
99
+ "ASYNC", # async rules
100
+ "B", # flake8-bugbear
101
+ "BLE", # flake8-blind-except
102
+ "C4", # flake8-comprehensions
103
+ "C90", # mccabe complexity
104
+ "DTZ", # flake8-datetimez
105
+ "E", # pycodestyle
106
+ "EM", # flake8-errmsg
107
+ "ERA", # eradicate (commented-out code)
108
+ "EXE", # flake8-executable
109
+ "F", # pyflakes/autoflake
110
+ "FA", # flake8-future-annotations
111
+ "FIX", # flake8-fixme
112
+ "FLY", # flynt
113
+ "FURB", # refurb
114
+ "G", # flake8-logging-format
115
+ "I", # isort
116
+ "ICN", # flake8-import-conventions
117
+ "INP", # flake8-no-pep420 (implicit namespace packages)
118
+ "ISC", # flake8-implicit-str-concat
119
+ "LOG", # flake8-logging
120
+ "N", # pep8-naming
121
+ "NPY", # numpy-specific rules
122
+ "PERF", # Perflint
123
+ "PGH", # pygrep-hooks
124
+ "PIE", # flake8-pie
125
+ "PL", # pylint
126
+ "PT", # flake8-pytest-style
127
+ "PTH", # flake8-use-pathlib
128
+ "PYI", # flake8-pyi
129
+ "Q", # flake8-quotes
130
+ "UP", # pyupgrade
131
+ "RET", # flake8-return
132
+ "RSE", # flake8-raise
133
+ "RUF", # ruff
134
+ "S", # flake8-bandit
135
+ "SIM", # flake8-SIM
136
+ "SLF", # flake8-self
137
+ "SLOT", # flake8-slots
138
+ "T10", # flake8-debugger
139
+ "T20", # flake8-print
140
+ "TC", # flake8-type-checking
141
+ "TD", # flake8-todos
142
+ "TID", # Tidy imports
143
+ "TRY", # try rules
144
+ "W", # pycodestyle warnings
145
+ "YTT", # flake8-2020
146
+ ]
147
+
148
+ ignore = [
149
+ "ASYNC109", # `timeout` parameters are part of the public async API
150
+ "E501", # line too long
151
+ "PLR0911", # Too many return statements
152
+ "PLR0912", # Too many branches
153
+ "PLR0913", # Too many arguments to function call
154
+ "PLR0915", # Too many statements
155
+ "PLR2004", # Magic value used in comparison
156
+ "PLW2901", # Outer variable overwritten by inner target
157
+ "TRY003", # Avoid specifying long messages outside the exception class
158
+ "TID252", # Prefer absolute imports over relative imports from parent modules
159
+ ]
160
+
161
+ [tool.ruff.lint.isort]
162
+ force-sort-within-sections = true
163
+ known-first-party = ["ulid_transform", "tests"]
164
+ combine-as-imports = true
165
+ split-on-trailing-comma = false
166
+
167
+ [tool.ruff.lint.per-file-ignores]
168
+ "bench/*" = ["S101", "SLF", "T20"]
169
+ "tests/*" = ["ARG", "S101", "S106", "S311", "SLF"]
170
+
171
+ [build-system]
172
+ requires = ['setuptools>=77.0', "poetry-core>=2.1.0"]
173
+ build-backend = "poetry.core.masonry.api"
@@ -12,9 +12,9 @@ package_data = \
12
12
 
13
13
  setup_kwargs = {
14
14
  'name': 'ulid-transform',
15
- 'version': '2.2.4',
15
+ 'version': '2.2.5',
16
16
  'description': 'Create and transform ULIDs',
17
- 'long_description': '# Fast ULID transformations\n\n<p align="center">\n <a href="https://github.com/bluetooth-devices/ulid-transform/actions/workflows/ci.yml?query=branch%3Amain">\n <img src="https://img.shields.io/github/actions/workflow/status/bluetooth-devices/ulid-transform/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >\n </a>\n <a href="https://codecov.io/gh/bluetooth-devices/ulid-transform">\n <img src="https://img.shields.io/codecov/c/github/bluetooth-devices/ulid-transform.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">\n </a>\n</p>\n<p align="center">\n <a href="https://python-poetry.org/">\n <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">\n </a>\n <a href="https://github.com/ambv/black">\n <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">\n </a>\n <a href="https://github.com/pre-commit/pre-commit">\n <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">\n </a>\n <a href="https://codspeed.io/Bluetooth-Devices/ulid-transform"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"/></a>\n</p>\n<p align="center">\n <a href="https://pypi.org/project/ulid-transform/">\n <img src="https://img.shields.io/pypi/v/ulid-transform.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">\n </a>\n <img src="https://img.shields.io/pypi/pyversions/ulid-transform.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">\n <img src="https://img.shields.io/pypi/l/ulid-transform.svg?style=flat-square" alt="License">\n</p>\n\nCreate and transform ULIDs\n\nThis library will use the C++ implementation from https://github.com/suyash/ulid when the C extension is available, and will fallback to pure python if it is not.\n\n## Example\n\n```python\n>>> import ulid_transform\n>>> ulid_transform.ulid_hex()\n\'01869a2ea5fb0b43aa056293e47c0a35\'\n>>> ulid_transform.ulid_now()\n\'0001HZX0NW00GW0X476W5TVBFE\'\n>>> ulid_transform.ulid_at_time(1234)\n\'000000016JC62D620DGYNG2R8H\'\n>>> ulid_transform.ulid_to_bytes(\'0001HZX0NW00GW0X476W5TVBFE\')\nb\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\'\n>> ulid_transform.bytes_to_ulid(b"\\x01\\x86\\x99?\\xe8\\xf3\\x11\\xbc\\xed\\xef\\x86U.9\\x03z")\n\'01GTCKZT7K26YEVVW6AMQ3J0VT\'\n>>> ulid_transform.ulid_to_bytes_or_none(\'0001HZX0NW00GW0X476W5TVBFE\')\nb\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\'\n>>> ulid_transform.ulid_to_bytes_or_none(None)\n>>> ulid_transform.bytes_to_ulid_or_none(b\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\')\n\'0001HZX0NW00GW0X476W5TVBFE\'\n>>> ulid_transform.bytes_to_ulid_or_none(None)\n```\n\n## Installation\n\nInstall this via pip (or your favourite package manager):\n\n`pip install ulid-transform`\n\n## Contributors ✨\n\nThanks to https://github.com/suyash/ulid which provides the C++ implementation guts.\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n<!-- prettier-ignore-start -->\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- markdownlint-disable -->\n<!-- markdownlint-enable -->\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n<!-- prettier-ignore-end -->\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n\n## Credits\n\nThis package was created with\n[Copier](https://copier.readthedocs.io/) and the\n[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)\nproject template.\n',
17
+ 'long_description': '# Fast ULID transformations\n\n<p align="center">\n <a href="https://github.com/bluetooth-devices/ulid-transform/actions/workflows/ci.yml?query=branch%3Amain">\n <img src="https://img.shields.io/github/actions/workflow/status/bluetooth-devices/ulid-transform/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >\n </a>\n <a href="https://codecov.io/gh/bluetooth-devices/ulid-transform">\n <img src="https://img.shields.io/codecov/c/github/bluetooth-devices/ulid-transform.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">\n </a>\n</p>\n<p align="center">\n <a href="https://python-poetry.org/">\n <img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">\n </a>\n <a href="https://github.com/ambv/black">\n <img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">\n </a>\n <a href="https://github.com/pre-commit/pre-commit">\n <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">\n </a>\n <a href="https://codspeed.io/Bluetooth-Devices/ulid-transform"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"/></a>\n</p>\n<p align="center">\n <a href="https://pypi.org/project/ulid-transform/">\n <img src="https://img.shields.io/pypi/v/ulid-transform.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">\n </a>\n <img src="https://img.shields.io/pypi/pyversions/ulid-transform.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">\n <img src="https://img.shields.io/pypi/l/ulid-transform.svg?style=flat-square" alt="License">\n</p>\n\nCreate and transform ULIDs\n\nThis library will use the C++ implementation from https://github.com/suyash/ulid when the C extension is available, and will fallback to pure python if it is not.\n\n## Example\n\n```python\n>>> import ulid_transform\n>>> ulid_transform.ulid_hex()\n\'01869a2ea5fb0b43aa056293e47c0a35\'\n>>> ulid_transform.ulid_now()\n\'0001HZX0NW00GW0X476W5TVBFE\'\n>>> ulid_transform.ulid_at_time(1234)\n\'000000016JC62D620DGYNG2R8H\'\n>>> ulid_transform.ulid_to_bytes(\'0001HZX0NW00GW0X476W5TVBFE\')\nb\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\'\n>>> ulid_transform.bytes_to_ulid(b"\\x01\\x86\\x99?\\xe8\\xf3\\x11\\xbc\\xed\\xef\\x86U.9\\x03z")\n\'01GTCKZT7K26YEVVW6AMQ3J0VT\'\n>>> ulid_transform.ulid_to_bytes_or_none(\'0001HZX0NW00GW0X476W5TVBFE\')\nb\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\'\n>>> ulid_transform.ulid_to_bytes_or_none(None)\n>>> ulid_transform.bytes_to_ulid_or_none(b\'\\x00\\x00c\\xfe\\x82\\xbc\\x00!\\xc0t\\x877\\x0b\\xad\\xad\\xee\')\n\'0001HZX0NW00GW0X476W5TVBFE\'\n>>> ulid_transform.bytes_to_ulid_or_none(None)\n```\n\n## Installation\n\nInstall this via pip (or your favourite package manager):\n\n`pip install ulid-transform`\n\n## Contributors ✨\n\nThanks to https://github.com/suyash/ulid which provides the C++ implementation guts.\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n<!-- prettier-ignore-start -->\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- markdownlint-disable -->\n<!-- markdownlint-enable -->\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n<!-- prettier-ignore-end -->\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n\n## Credits\n\nThis package was created with\n[Copier](https://copier.readthedocs.io/) and the\n[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)\nproject template.\n',
18
18
  'author': 'J. Nick Koston',
19
19
  'author_email': 'nick@koston.org',
20
20
  'maintainer': 'None',
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.4"
1
+ __version__ = "2.2.5"
2
2
 
3
3
  try:
4
4
  from ._ulid_impl import (
@@ -287,6 +287,9 @@ def ulid_at_time_bytes(timestamp: float) -> bytes:
287
287
 
288
288
  uuid.UUID(bytes=ulid_bytes)
289
289
  """
290
+ if not isinstance(timestamp, (int, float)):
291
+ msg = f"must be real number, not {type(timestamp).__name__}" # type: ignore[unreachable]
292
+ raise TypeError(msg)
290
293
  return int(timestamp * 1000).to_bytes(6, byteorder="big") + int(
291
294
  getrandbits(80)
292
295
  ).to_bytes(10, byteorder="big")
@@ -363,9 +366,18 @@ def _encode(ulid_bytes: bytes) -> str:
363
366
  def ulid_to_bytes(value: str) -> bytes:
364
367
  """Decode a ulid to bytes."""
365
368
  if not isinstance(value, str):
366
- raise TypeError(f"ULID must be a string, not {type(value).__name__}")
367
- if len(value) != 26:
368
- raise ValueError(f"ULID must be a 26 character string: {value}")
369
+ msg = f"ULID must be a string, not {type(value).__name__}" # type: ignore[unreachable]
370
+ raise TypeError(msg)
371
+ if len(value) != 26 or not value.isascii():
372
+ # The C extension measures length in UTF-8 bytes via
373
+ # PyUnicode_AsUTF8AndSize, so any non-ASCII codepoint pushes the byte
374
+ # length past 26 and is rejected with ValueError there. Match that: a
375
+ # non-ASCII string is never a valid 26-character ULID, and folding the
376
+ # check here keeps the exception type aligned (C raises ValueError; the
377
+ # bare value.encode("ascii") below would otherwise raise
378
+ # UnicodeEncodeError).
379
+ msg = f"ULID must be a 26 character string: {value}"
380
+ raise ValueError(msg)
369
381
  encoded = value.encode("ascii")
370
382
  decoding = _DECODE
371
383
  return bytes(
@@ -423,9 +435,11 @@ def ulid_to_bytes(value: str) -> bytes:
423
435
  def bytes_to_ulid(value: bytes) -> str:
424
436
  """Encode bytes to a ulid."""
425
437
  if not isinstance(value, bytes):
426
- raise TypeError(f"ULID bytes must be bytes, not {type(value).__name__}")
438
+ msg = f"ULID bytes must be bytes, not {type(value).__name__}" # type: ignore[unreachable]
439
+ raise TypeError(msg)
427
440
  if len(value) != 16:
428
- raise ValueError(f"ULID bytes must be 16 bytes: {value!r}")
441
+ msg = f"ULID bytes must be 16 bytes: {value!r}"
442
+ raise ValueError(msg)
429
443
  return _encode(value)
430
444
 
431
445
 
@@ -453,10 +467,12 @@ def ulid_to_timestamp(ulid: str | bytes) -> int:
453
467
  """
454
468
  if isinstance(ulid, bytes):
455
469
  if len(ulid) != 16:
456
- raise ValueError(f"ULID bytes must be 16 bytes: {ulid!r}")
470
+ msg = f"ULID bytes must be 16 bytes: {ulid!r}"
471
+ raise ValueError(msg)
457
472
  ulid_bytes = ulid
458
473
  elif isinstance(ulid, str):
459
474
  ulid_bytes = ulid_to_bytes(ulid)
460
475
  else:
461
- raise TypeError(f"ULID must be a string or bytes, not {type(ulid).__name__}")
476
+ msg = f"ULID must be a string or bytes, not {type(ulid).__name__}" # type: ignore[unreachable]
477
+ raise TypeError(msg)
462
478
  return int.from_bytes(b"\x00\x00" + ulid_bytes[:6], "big")
@@ -1,5 +1,6 @@
1
1
  #include "Python.h"
2
2
 
3
+ #include <cmath>
3
4
  #include <string.h>
4
5
 
5
6
  #ifdef __SIZEOF_INT128__
@@ -90,6 +91,42 @@ py_ulid_now_bytes(PyObject* module, PyObject* Py_UNUSED(ignored))
90
91
  return PyBytes_FromStringAndSize((const char*)buf, ULID_BYTES_LEN);
91
92
  }
92
93
 
94
+ /* Validate a timestamp (in seconds) and convert to milliseconds.
95
+ * Returns 0 on success, -1 on error (with a Python exception set).
96
+ * Error types/messages mirror what int(ts * 1000).to_bytes(6, 'big') raises
97
+ * in the Python implementation, so the two impls have exception-type parity.
98
+ */
99
+ static inline int
100
+ validate_timestamp_ms(double ts, int64_t* out_ms)
101
+ {
102
+ if (std::isnan(ts)) {
103
+ PyErr_SetString(PyExc_ValueError,
104
+ "cannot convert float NaN to integer");
105
+ return -1;
106
+ }
107
+ if (std::isinf(ts)) {
108
+ PyErr_SetString(PyExc_OverflowError,
109
+ "cannot convert float infinity to integer");
110
+ return -1;
111
+ }
112
+ double ts_ms = ts * 1000.0;
113
+ if (ts_ms < 0.0) {
114
+ PyErr_SetString(PyExc_OverflowError,
115
+ "can't convert negative int to unsigned");
116
+ return -1;
117
+ }
118
+ // ULID timestamps are 48-bit unsigned milliseconds.
119
+ // 2^48 (281474976710656) is the exclusive upper bound and is exactly
120
+ // representable as a double, so the comparison is precise here.
121
+ if (ts_ms >= 281474976710656.0) {
122
+ PyErr_SetString(PyExc_OverflowError,
123
+ "int too big to convert");
124
+ return -1;
125
+ }
126
+ *out_ms = static_cast<int64_t>(ts_ms);
127
+ return 0;
128
+ }
129
+
93
130
  /* ulid_at_time_bytes(timestamp) -> bytes */
94
131
  static PyObject*
95
132
  py_ulid_at_time_bytes(PyObject* module, PyObject* arg)
@@ -97,8 +134,11 @@ py_ulid_at_time_bytes(PyObject* module, PyObject* arg)
97
134
  double ts = PyFloat_AsDouble(arg);
98
135
  if (ts == -1.0 && PyErr_Occurred())
99
136
  return NULL;
137
+ int64_t ts_ms;
138
+ if (validate_timestamp_ms(ts, &ts_ms) < 0)
139
+ return NULL;
100
140
  ulid::ULID ulid;
101
- ulid::EncodeTimestamp(static_cast<int64_t>(ts * 1000), ulid);
141
+ ulid::EncodeTimestamp(ts_ms, ulid);
102
142
  ulid::EncodeEntropyFast(ulid);
103
143
  uint8_t buf[ULID_BYTES_LEN];
104
144
  ulid::MarshalBinaryTo(ulid, buf);
@@ -124,8 +164,11 @@ py_ulid_at_time(PyObject* module, PyObject* arg)
124
164
  double ts = PyFloat_AsDouble(arg);
125
165
  if (ts == -1.0 && PyErr_Occurred())
126
166
  return NULL;
167
+ int64_t ts_ms;
168
+ if (validate_timestamp_ms(ts, &ts_ms) < 0)
169
+ return NULL;
127
170
  ulid::ULID ulid;
128
- ulid::EncodeTimestamp(static_cast<int64_t>(ts * 1000), ulid);
171
+ ulid::EncodeTimestamp(ts_ms, ulid);
129
172
  ulid::EncodeEntropyFast(ulid);
130
173
  char buf[ULID_TEXT_LEN];
131
174
  ulid::MarshalTo(ulid, buf);
@@ -1,108 +0,0 @@
1
- [project]
2
- name = "ulid-transform"
3
- version = "2.2.4"
4
- license = "MIT"
5
- description = "Create and transform ULIDs"
6
- readme = "README.md"
7
- authors = [{ name = "J. Nick Koston", email = "nick@koston.org" }]
8
- requires-python = ">=3.11"
9
-
10
- [project.urls]
11
- "Repository" = "https://github.com/bluetooth-devices/ulid-transform"
12
- "Bug Tracker" = "https://github.com/bluetooth-devices/ulid-transform/issues"
13
- "Changelog" = "https://github.com/bluetooth-devices/ulid-transform/blob/main/CHANGELOG.md"
14
-
15
- [tool.poetry]
16
- classifiers = [
17
- "Development Status :: 5 - Production/Stable",
18
- "Intended Audience :: Developers",
19
- "Natural Language :: English",
20
- "Operating System :: OS Independent",
21
- "Topic :: Software Development :: Libraries",
22
- ]
23
- packages = [
24
- { include = "ulid_transform", from = "src" },
25
- ]
26
-
27
- [tool.poetry.build]
28
- generate-setup-file = true
29
- script = "build_ext.py"
30
-
31
- [tool.poetry.dependencies]
32
- python = "^3.11"
33
-
34
- [tool.poetry.group.dev.dependencies]
35
- pytest = ">=9.0.3,<10"
36
- pytest-cov = ">=3,<8"
37
- setuptools = ">=65.4.1,<83.0.0"
38
- pytest-codspeed = ">=5.0.2,<6.0.0"
39
-
40
-
41
- [tool.poetry.group.benchmark.dependencies]
42
- ulid-py = "^1.1.0"
43
- ulid2 = "^0.3.0"
44
- pytest-benchmark = ">=4,<6"
45
-
46
- [tool.semantic_release]
47
- version_toml = ["pyproject.toml:project.version"]
48
- version_variables = [
49
- "src/ulid_transform/__init__.py:__version__",
50
- ]
51
- build_command = "pip install poetry && poetry build"
52
-
53
- [tool.pytest.ini_options]
54
- pythonpath = ["src"]
55
-
56
- [tool.coverage.run]
57
- branch = true
58
-
59
- [tool.coverage.report]
60
- exclude_lines = [
61
- "pragma: no cover",
62
- "@overload",
63
- "if TYPE_CHECKING",
64
- "raise NotImplementedError",
65
- 'if __name__ == "__main__":',
66
- ]
67
-
68
- [tool.mypy]
69
- check_untyped_defs = true
70
- disallow_any_generics = true
71
- disallow_incomplete_defs = true
72
- disallow_untyped_defs = true
73
- mypy_path = "src/"
74
- no_implicit_optional = true
75
- show_error_codes = true
76
- warn_unreachable = true
77
- warn_unused_ignores = true
78
- exclude = [
79
- 'setup.py',
80
- ]
81
-
82
- [[tool.mypy.overrides]]
83
- module = "tests.*"
84
- allow_untyped_defs = true
85
- warn_unused_ignores = false
86
-
87
- [[tool.mypy.overrides]]
88
- module = "bench.*"
89
- allow_untyped_defs = true
90
-
91
- [tool.ruff.lint]
92
- extend-select = [
93
- "B",
94
- "I",
95
- "S",
96
- "UP",
97
- ]
98
-
99
- [tool.ruff.lint.isort]
100
- known-first-party = ["ulid_transform", "tests"]
101
-
102
- [tool.ruff.lint.per-file-ignores]
103
- "bench/*" = ["S101"]
104
- "tests/*" = ["S101", "S311"]
105
-
106
- [build-system]
107
- requires = ['setuptools>=77.0', "poetry-core>=2.1.0"]
108
- build-backend = "poetry.core.masonry.api"
File without changes