python-ulid 2.2.0__tar.gz → 2.4.0__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.
Files changed (30) hide show
  1. {python_ulid-2.2.0 → python_ulid-2.4.0}/.github/workflows/lint-and-test.yml +19 -6
  2. {python_ulid-2.2.0 → python_ulid-2.4.0}/.github/workflows/publish.yml +1 -1
  3. {python_ulid-2.2.0 → python_ulid-2.4.0}/.gitignore +1 -1
  4. python_ulid-2.4.0/.pre-commit-config.yaml +10 -0
  5. {python_ulid-2.2.0 → python_ulid-2.4.0}/.readthedocs.yml +3 -3
  6. {python_ulid-2.2.0 → python_ulid-2.4.0}/CHANGELOG.rst +25 -0
  7. {python_ulid-2.2.0 → python_ulid-2.4.0}/PKG-INFO +65 -6
  8. {python_ulid-2.2.0 → python_ulid-2.4.0}/README.rst +57 -4
  9. {python_ulid-2.2.0 → python_ulid-2.4.0}/docs/Makefile +2 -2
  10. python_ulid-2.4.0/docs/requirements.txt +2 -0
  11. python_ulid-2.4.0/docs/source/changelog.rst +1 -0
  12. {python_ulid-2.2.0/docs → python_ulid-2.4.0/docs/source}/conf.py +5 -11
  13. {python_ulid-2.2.0/docs → python_ulid-2.4.0/docs/source}/index.rst +8 -4
  14. {python_ulid-2.2.0 → python_ulid-2.4.0}/hatch.toml +13 -9
  15. {python_ulid-2.2.0 → python_ulid-2.4.0}/pyproject.toml +16 -10
  16. python_ulid-2.4.0/tests/test_cli.py +77 -0
  17. {python_ulid-2.2.0 → python_ulid-2.4.0}/tests/test_ulid.py +34 -1
  18. {python_ulid-2.2.0 → python_ulid-2.4.0}/ulid/__init__.py +58 -9
  19. {python_ulid-2.2.0 → python_ulid-2.4.0}/ulid/__main__.py +26 -27
  20. {python_ulid-2.2.0 → python_ulid-2.4.0}/ulid/constants.py +1 -0
  21. python_ulid-2.2.0/.pre-commit-config.yaml +0 -18
  22. python_ulid-2.2.0/docs/changelog.rst +0 -1
  23. python_ulid-2.2.0/docs/requirements.txt +0 -1
  24. {python_ulid-2.2.0 → python_ulid-2.4.0}/LICENSE +0 -0
  25. {python_ulid-2.2.0/docs → python_ulid-2.4.0/docs/source}/api.rst +0 -0
  26. {python_ulid-2.2.0 → python_ulid-2.4.0}/logo.png +0 -0
  27. {python_ulid-2.2.0 → python_ulid-2.4.0}/tests/__init__.py +0 -0
  28. {python_ulid-2.2.0 → python_ulid-2.4.0}/tests/test_base32.py +0 -0
  29. {python_ulid-2.2.0 → python_ulid-2.4.0}/ulid/base32.py +0 -0
  30. {python_ulid-2.2.0 → python_ulid-2.4.0}/ulid/py.typed +0 -0
@@ -9,12 +9,13 @@ on:
9
9
  jobs:
10
10
  lint-code:
11
11
  runs-on: ubuntu-latest
12
+ name: Lint code
12
13
  steps:
13
14
  - uses: actions/checkout@v4
14
15
  - name: Set up Python
15
- uses: actions/setup-python@v4
16
+ uses: actions/setup-python@v5
16
17
  with:
17
- python-version: "3.11"
18
+ python-version: "3.12"
18
19
  - name: Install dependencies
19
20
  run: |
20
21
  pip install hatch
@@ -25,12 +26,13 @@ jobs:
25
26
 
26
27
  lint-typing:
27
28
  runs-on: ubuntu-latest
29
+ name: Lint type annotations
28
30
  steps:
29
31
  - uses: actions/checkout@v4
30
32
  - name: Set up Python
31
- uses: actions/setup-python@v4
33
+ uses: actions/setup-python@v5
32
34
  with:
33
- python-version: "3.11"
35
+ python-version: "3.12"
34
36
  - name: Install dependencies
35
37
  run: |
36
38
  pip install hatch
@@ -41,13 +43,14 @@ jobs:
41
43
 
42
44
  test:
43
45
  runs-on: ubuntu-latest
46
+ name: Run tests
44
47
  strategy:
45
48
  matrix:
46
- python-version: ["3.9", "3.10", "3.11"]
49
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
47
50
  steps:
48
51
  - uses: actions/checkout@v4
49
52
  - name: Set up Python ${{ matrix.python-version }}
50
- uses: actions/setup-python@v4
53
+ uses: actions/setup-python@v5
51
54
  with:
52
55
  python-version: ${{ matrix.python-version }}
53
56
  - name: Install dependencies
@@ -57,3 +60,13 @@ jobs:
57
60
  pip install hatch-fancy-pypi-readme
58
61
  - name: Test
59
62
  run: hatch run cov-test
63
+ - uses: codecov/codecov-action@v4
64
+ with:
65
+ token: ${{ secrets.CODECOV_TOKEN }}
66
+
67
+ package:
68
+ name: Build & verify package
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@v4
72
+ - uses: hynek/build-and-inspect-python-package@v2
@@ -11,7 +11,7 @@ jobs:
11
11
  steps:
12
12
  - uses: actions/checkout@v4
13
13
  - name: Set up Python
14
- uses: actions/setup-python@v4
14
+ uses: actions/setup-python@v5
15
15
  with:
16
16
  python-version: "3.x"
17
17
  - name: Install dependencies
@@ -12,4 +12,4 @@ __pycache__
12
12
  /ChangeLog
13
13
  /dist/
14
14
  /build/
15
- /docs/_build
15
+ /docs/build
@@ -0,0 +1,10 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.3.3
4
+ hooks:
5
+ - id: ruff
6
+ - id: ruff-format
7
+ - repo: https://github.com/pycqa/doc8
8
+ rev: v1.1.1
9
+ hooks:
10
+ - id: doc8
@@ -1,12 +1,12 @@
1
1
  version: 2
2
2
 
3
3
  build:
4
- os: ubuntu-20.04
4
+ os: ubuntu-22.04
5
5
  tools:
6
- python: "3.9"
6
+ python: "3.11"
7
7
 
8
8
  sphinx:
9
- configuration: docs/conf.py
9
+ configuration: docs/source/conf.py
10
10
 
11
11
  python:
12
12
  install:
@@ -5,6 +5,29 @@ Changelog
5
5
 
6
6
  Versions follow `Semantic Versioning <http://www.semver.org>`_
7
7
 
8
+ `2.4.0`_ - 2024-04-02
9
+ ---------------------
10
+
11
+ Added
12
+ ~~~~~
13
+ * :class:`.ULID` objects are now properly serialized when used as Pydantic types `@Avihais12344 <https://github.com/Avihais12344>`_.
14
+
15
+
16
+ `2.3.0`_ - 2024-03-21
17
+ ---------------------
18
+
19
+ Added
20
+ ~~~~~
21
+ * :class:`.ULID` objects can now be converted to bytes with ``bytes(ulid)``.
22
+ * The Pydantic v2 protocol is now supported, so that the :class:`.ULID` class can be directly used
23
+ as type annotations in `Pydantic models <https://docs.pydantic.dev/latest/concepts/models/#basic-model-usage>`_
24
+
25
+ Changed
26
+ ~~~~~~~
27
+ * The type annotations have been adapted, so that the classmethod constructors properly reflect the
28
+ type for :class:`.ULID` subclasses. Thanks to `@johnpaulett <https://github.com/johnpaulett>`_
29
+
30
+
8
31
  `2.2.0`_ - 2023-09-21
9
32
  ---------------------
10
33
 
@@ -128,6 +151,8 @@ Changed
128
151
  * The package now has no external dependencies.
129
152
  * The test-coverage has been raised to 100%.
130
153
 
154
+ .. _2.4.0: https://github.com/mdomke/python-ulid/compare/2.3.0...2.4.0
155
+ .. _2.3.0: https://github.com/mdomke/python-ulid/compare/2.2.0...2.3.0
131
156
  .. _2.2.0: https://github.com/mdomke/python-ulid/compare/2.1.0...2.2.0
132
157
  .. _2.1.0: https://github.com/mdomke/python-ulid/compare/2.0.0...2.1.0
133
158
  .. _2.0.0: https://github.com/mdomke/python-ulid/compare/1.1.0...2.0.0
@@ -1,7 +1,10 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-ulid
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: Universally unique lexicographically sortable identifier
5
+ Project-URL: Homepage, https://github.com/mdomke/python-ulid
6
+ Project-URL: Repository, https://github.com/mdomke/python-ulid
7
+ Project-URL: Documentation, https://python-ulid.readthedocs.io/
5
8
  Author-email: Martin Domke <mail@martindomke.net>
6
9
  License-Expression: MIT
7
10
  License-File: LICENSE
@@ -15,8 +18,11 @@ Classifier: Programming Language :: Python
15
18
  Classifier: Programming Language :: Python :: 3.9
16
19
  Classifier: Programming Language :: Python :: 3.10
17
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
18
22
  Classifier: Topic :: Software Development :: Libraries
19
23
  Requires-Python: >=3.9
24
+ Provides-Extra: pydantic
25
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
20
26
  Description-Content-Type: text/x-rst
21
27
 
22
28
 
@@ -57,6 +63,12 @@ Use ``pip`` to install the library
57
63
 
58
64
  $ pip install python-ulid
59
65
 
66
+ to include Pydantic support install the optional dependency like so
67
+
68
+ .. code-block:: bash
69
+
70
+ $ pip install python-ulid[pydantic]
71
+
60
72
  .. installation-end
61
73
 
62
74
  .. usage-begin
@@ -82,8 +94,8 @@ or use one of the named constructors
82
94
  >>> ULID.from_datetime(datetime.datetime.now())
83
95
  ULID(01E75J2XBK390V2XRH44EHC10X)
84
96
 
85
- There are several options for encoding the ``ULID`` object (e.g. string, hex, int),
86
- as well as to access the timestamp attribute in different formats:
97
+ There are several options for encoding the ``ULID`` object
98
+ (e.g. string, hex, int, bytes, UUID):
87
99
 
88
100
  .. code-block:: pycon
89
101
 
@@ -91,15 +103,47 @@ as well as to access the timestamp attribute in different formats:
91
103
  '01BTGNYV6HRNK8K8VKZASZCFPE'
92
104
  >>> ulid.hex
93
105
  '015ea15f6cd1c56689a373fab3f63ece'
106
+ >>> int(ulid)
107
+ 1820576928786795198723644692628913870
108
+ >>> bytes(ulid)
109
+ b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
110
+ >>> ulid.to_uuid()
111
+ UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
112
+
113
+ It is also possible to directly access the timestamp component of a ``ULID``,
114
+ either in UNIX epoch or as ``datetime.datetime``
115
+
116
+ .. code-block:: pycon
117
+
94
118
  >>> ulid.timestamp
95
119
  1505945939.153
96
120
  >>> ulid.datetime
97
121
  datetime.datetime(2017, 9, 20, 22, 18, 59, 153000, tzinfo=datetime.timezone.utc)
98
- >>> ulid.to_uuid()
99
- UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
100
122
 
101
123
  .. usage-end
102
124
 
125
+ .. pydantic-begin
126
+
127
+ Pydantic integration
128
+ ---------------------
129
+
130
+ The ``ULID`` class can be directly used for the popular data validation library
131
+ `Pydantic <https://docs.pydantic.dev/latest/>`_ like so
132
+
133
+ .. code-block:: python
134
+
135
+ from pydantic import BaseModel
136
+ from ulid import ULID
137
+
138
+
139
+ class Model(BaseModel):
140
+ ulid: ULID
141
+
142
+ model = Model(ulid="DX89370400440532013000") # OK
143
+ model = Model(ulid="not-a-ulid") # Raises ValidationError
144
+
145
+ .. pydantic-end
146
+
103
147
  .. cli-begin
104
148
 
105
149
  Command line interface
@@ -124,6 +168,14 @@ and convert ULIDs, e.g.
124
168
  Timestamp: 1695219822.248
125
169
  Datetime: 2023-09-20 14:23:42.248000+00:00
126
170
 
171
+ There are several flags to select specific output formats for the ``show`` command, e.g.
172
+
173
+
174
+ .. code-block:: bash
175
+
176
+ $ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
177
+ 2023-09-20 14:23:42.248000+00:00
178
+
127
179
  The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
128
180
 
129
181
  .. code-block:: bash
@@ -134,6 +186,9 @@ The special character ``-`` allows to read values from ``stdin`` so that they ca
134
186
  $ date --iso-8601 | python -m ulid build --from-datetime -
135
187
  01HAT9PVR02T3S13XB48S7GEHE
136
188
 
189
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
190
+ (e.g. ``ulid show --help``).
191
+
137
192
  .. cli-end
138
193
 
139
194
  Other implementations
@@ -142,3 +197,7 @@ Other implementations
142
197
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
143
198
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
144
199
  * `mdipierro/ulid <https://github.com/mdipierro/ulid>`_
200
+ * `oklog/ulid <https://github.com/oklog/ulid>`_
201
+ * `ulid/javascript <https://github.com/ulid/javascript>`_
202
+ * `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
203
+ * `imdario/go-ulid <https://github.com/imdario/go-ulid>`_
@@ -69,6 +69,12 @@ Use ``pip`` to install the library
69
69
 
70
70
  $ pip install python-ulid
71
71
 
72
+ to include Pydantic support install the optional dependency like so
73
+
74
+ .. code-block:: bash
75
+
76
+ $ pip install python-ulid[pydantic]
77
+
72
78
  .. installation-end
73
79
 
74
80
  .. usage-begin
@@ -94,8 +100,8 @@ or use one of the named constructors
94
100
  >>> ULID.from_datetime(datetime.datetime.now())
95
101
  ULID(01E75J2XBK390V2XRH44EHC10X)
96
102
 
97
- There are several options for encoding the ``ULID`` object (e.g. string, hex, int),
98
- as well as to access the timestamp attribute in different formats:
103
+ There are several options for encoding the ``ULID`` object
104
+ (e.g. string, hex, int, bytes, UUID):
99
105
 
100
106
  .. code-block:: pycon
101
107
 
@@ -103,15 +109,47 @@ as well as to access the timestamp attribute in different formats:
103
109
  '01BTGNYV6HRNK8K8VKZASZCFPE'
104
110
  >>> ulid.hex
105
111
  '015ea15f6cd1c56689a373fab3f63ece'
112
+ >>> int(ulid)
113
+ 1820576928786795198723644692628913870
114
+ >>> bytes(ulid)
115
+ b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
116
+ >>> ulid.to_uuid()
117
+ UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
118
+
119
+ It is also possible to directly access the timestamp component of a ``ULID``,
120
+ either in UNIX epoch or as ``datetime.datetime``
121
+
122
+ .. code-block:: pycon
123
+
106
124
  >>> ulid.timestamp
107
125
  1505945939.153
108
126
  >>> ulid.datetime
109
127
  datetime.datetime(2017, 9, 20, 22, 18, 59, 153000, tzinfo=datetime.timezone.utc)
110
- >>> ulid.to_uuid()
111
- UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
112
128
 
113
129
  .. usage-end
114
130
 
131
+ .. pydantic-begin
132
+
133
+ Pydantic integration
134
+ ---------------------
135
+
136
+ The ``ULID`` class can be directly used for the popular data validation library
137
+ `Pydantic <https://docs.pydantic.dev/latest/>`_ like so
138
+
139
+ .. code-block:: python
140
+
141
+ from pydantic import BaseModel
142
+ from ulid import ULID
143
+
144
+
145
+ class Model(BaseModel):
146
+ ulid: ULID
147
+
148
+ model = Model(ulid="DX89370400440532013000") # OK
149
+ model = Model(ulid="not-a-ulid") # Raises ValidationError
150
+
151
+ .. pydantic-end
152
+
115
153
  .. cli-begin
116
154
 
117
155
  Command line interface
@@ -136,6 +174,14 @@ and convert ULIDs, e.g.
136
174
  Timestamp: 1695219822.248
137
175
  Datetime: 2023-09-20 14:23:42.248000+00:00
138
176
 
177
+ There are several flags to select specific output formats for the ``show`` command, e.g.
178
+
179
+
180
+ .. code-block:: bash
181
+
182
+ $ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
183
+ 2023-09-20 14:23:42.248000+00:00
184
+
139
185
  The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
140
186
 
141
187
  .. code-block:: bash
@@ -146,6 +192,9 @@ The special character ``-`` allows to read values from ``stdin`` so that they ca
146
192
  $ date --iso-8601 | python -m ulid build --from-datetime -
147
193
  01HAT9PVR02T3S13XB48S7GEHE
148
194
 
195
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
196
+ (e.g. ``ulid show --help``).
197
+
149
198
  .. cli-end
150
199
 
151
200
  Other implementations
@@ -154,3 +203,7 @@ Other implementations
154
203
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
155
204
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
156
205
  * `mdipierro/ulid <https://github.com/mdipierro/ulid>`_
206
+ * `oklog/ulid <https://github.com/oklog/ulid>`_
207
+ * `ulid/javascript <https://github.com/ulid/javascript>`_
208
+ * `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
209
+ * `imdario/go-ulid <https://github.com/imdario/go-ulid>`_
@@ -5,8 +5,8 @@
5
5
  # from the environment for the first two.
6
6
  SPHINXOPTS ?=
7
7
  SPHINXBUILD ?= sphinx-build
8
- SOURCEDIR = .
9
- BUILDDIR = _build
8
+ SOURCEDIR = source
9
+ BUILDDIR = build
10
10
 
11
11
  # Put it first so that "make" without argument is like "make help".
12
12
  help:
@@ -0,0 +1,2 @@
1
+ furo
2
+ sphinx-copybutton
@@ -0,0 +1 @@
1
+ .. include:: ../../CHANGELOG.rst
@@ -3,9 +3,9 @@ import sys
3
3
  from datetime import datetime
4
4
 
5
5
 
6
- sys.path.insert(0, os.path.abspath(".."))
6
+ sys.path.insert(0, os.path.abspath("../.."))
7
7
 
8
- import ulid # noqa
8
+ import ulid
9
9
 
10
10
 
11
11
  copyright = f"{datetime.now().year}, Martin Domke"
@@ -24,6 +24,7 @@ extensions = [
24
24
  "sphinx.ext.autodoc",
25
25
  "sphinx.ext.napoleon",
26
26
  "sphinx.ext.viewcode",
27
+ "sphinx_copybutton",
27
28
  ]
28
29
 
29
30
  # Add any paths that contain templates here, relative to this directory.
@@ -32,7 +33,7 @@ templates_path = ["_templates"]
32
33
  # List of patterns, relative to source directory, that match files and
33
34
  # directories to ignore when looking for source files.
34
35
  # This pattern also affects html_static_path and html_extra_path.
35
- exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
36
+ exclude_patterns = []
36
37
 
37
38
 
38
39
  # -- Options for HTML output -------------------------------------------------
@@ -40,15 +41,8 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
40
41
  # The theme to use for HTML and HTML Help pages. See the documentation for
41
42
  # a list of builtin themes.
42
43
  #
43
- html_logo = "../logo.png"
44
+ html_logo = "../../logo.png"
44
45
  html_theme = "furo"
45
- html_theme_options = {
46
- "github_user": "mdomke",
47
- "github_repo": "python-ulid",
48
- "description": "A library for working with ULIDs",
49
- "sidebar_collapse": False,
50
- "logo_text_align": "center",
51
- }
52
46
 
53
47
  html_title = "Python ULID docs"
54
48
 
@@ -4,19 +4,23 @@ Python ULID
4
4
  Release v\ |release| (:ref:`What's new <changelog>`)
5
5
 
6
6
 
7
- .. include:: ../README.rst
7
+ .. include:: ../../README.rst
8
8
  :start-after: teaser-begin
9
9
  :end-before: teaser-end
10
10
 
11
- .. include:: ../README.rst
11
+ .. include:: ../../README.rst
12
12
  :start-after: installation-begin
13
13
  :end-before: installation-end
14
14
 
15
- .. include:: ../README.rst
15
+ .. include:: ../../README.rst
16
16
  :start-after: usage-begin
17
17
  :end-before: usage-end
18
18
 
19
- .. include:: ../README.rst
19
+ .. include:: ../../README.rst
20
+ :start-after: pydantic-begin
21
+ :end-before: pydantic-end
22
+
23
+ .. include:: ../../README.rst
20
24
  :start-after: cli-begin
21
25
  :end-before: cli-end
22
26
 
@@ -1,8 +1,11 @@
1
1
  [envs.default]
2
2
  dependencies = [
3
- "freezegun==1.2.*",
3
+ "freezegun==1.4.*",
4
4
  "pytest-cov==4.1.*",
5
- "pytest==7.3.*",
5
+ "pytest==8.1.*",
6
+ ]
7
+ features = [
8
+ "pydantic"
6
9
  ]
7
10
 
8
11
  [envs.default.scripts]
@@ -11,18 +14,19 @@ cov-test = "pytest --cov {args:ulid} --cov-report=term-missing --cov-report=xml"
11
14
 
12
15
  [envs.lint]
13
16
  dependencies = [
14
- "ruff==0.0.290",
15
- "black==23.9.*",
16
- "mypy==1.5.*",
17
+ "ruff==0.3.*",
18
+ "mypy==1.9.*",
19
+ "doc8==1.1.*",
17
20
  ]
18
21
 
19
22
  [envs.lint.scripts]
20
23
  typing = "mypy --install-types --non-interactive {args:ulid}"
21
24
  style = [
22
- "black --check --diff {args:.}",
23
- "ruff {args:.}",
25
+ "ruff format --check --diff {args:.}",
26
+ "ruff check {args:.}",
24
27
  ]
25
28
  fmt = [
26
- "black {args:.}",
27
- "ruff --fix {args:.}",
29
+ "ruff format {args:.}",
30
+ "ruff check --fix {args:.}",
28
31
  ]
32
+ docs = "doc8 docs"
@@ -22,9 +22,20 @@ classifiers = [
22
22
  "Programming Language :: Python :: 3.9",
23
23
  "Programming Language :: Python :: 3.10",
24
24
  "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
25
26
  "Topic :: Software Development :: Libraries",
26
27
  ]
27
28
 
29
+ [project.urls]
30
+ Homepage = "https://github.com/mdomke/python-ulid"
31
+ Repository = "https://github.com/mdomke/python-ulid"
32
+ Documentation = "https://python-ulid.readthedocs.io/"
33
+
34
+ [project.optional-dependencies]
35
+ pydantic = [
36
+ "pydantic>=2.0"
37
+ ]
38
+
28
39
  [project.scripts]
29
40
  ulid = "ulid.__main__:entrypoint"
30
41
 
@@ -42,6 +53,8 @@ line-length = 100
42
53
  [tool.ruff]
43
54
  target-version = "py39"
44
55
  line-length = 100
56
+
57
+ [tool.ruff.lint]
45
58
  select = ["A", "B", "C", "C4", "E", "F", "I", "N", "PT", "Q", "RUF", "S", "SIM", "T10", "UP", "W", "YTT"]
46
59
  fixable = ["RUF100", "I001"]
47
60
  ignore = [
@@ -50,21 +63,14 @@ ignore = [
50
63
  "A003", # Allow shadowing bultins on classes
51
64
  ]
52
65
 
53
- [tool.ruff.mccabe]
66
+ [tool.ruff.lint.mccabe]
54
67
  max-complexity = 15
55
68
 
56
- [tool.ruff.isort]
69
+ [tool.ruff.lint.isort]
57
70
  force-single-line = true
58
71
  lines-after-imports = 2
59
72
  order-by-type = false
60
73
 
61
- [tool.isort]
62
- profile = "black"
63
- force_alphabetical_sort_within_sections = true
64
- force_single_line = true
65
- lines_after_imports = 2
66
- line_length = 100
67
-
68
74
  [tool.coverage.run]
69
75
  branch = true
70
76
  parallel = true
@@ -78,4 +84,4 @@ content-type = "text/x-rst"
78
84
 
79
85
  [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
80
86
  path = "README.rst"
81
- start-after = ".. teaser-begin"
87
+ start-after = ".. teaser-begin"
@@ -0,0 +1,77 @@
1
+ import pytest
2
+
3
+ import ulid.__main__ as cli
4
+ from ulid import ULID
5
+
6
+
7
+ @pytest.mark.parametrize("option", ["", "uuid", "uuid4", "hex", "int", "timestamp", "datetime"])
8
+ def test_parse_show(option: str):
9
+ ulid = ULID()
10
+ argv = ["show", f"--{option}", str(ulid)]
11
+ output = cli.main(argv)
12
+ if option == "uuid":
13
+ assert output == str(ulid.to_uuid())
14
+ elif option == "uuid4":
15
+ assert output == str(ulid.to_uuid4())
16
+ elif option == "hex":
17
+ assert output == ulid.hex
18
+ elif option == "int":
19
+ assert output == str(int(ulid))
20
+ elif option == "timestamp":
21
+ assert output == str(ulid.timestamp)
22
+ elif option == "datetime":
23
+ assert output == ulid.datetime.isoformat()
24
+ else:
25
+ assert str(ulid) in output
26
+ assert ulid.hex in output
27
+ assert str(int(ulid)) in output
28
+ assert str(ulid.timestamp) in output
29
+ assert str(ulid.datetime.isoformat()) in output
30
+
31
+
32
+ @pytest.mark.parametrize(
33
+ "option",
34
+ [
35
+ "",
36
+ "from-uuid",
37
+ "from-str",
38
+ "from-hex",
39
+ "from-int",
40
+ "from-timestamp",
41
+ "from-datetime",
42
+ ],
43
+ )
44
+ def test_build(option: str):
45
+ ulid = ULID()
46
+ value: str
47
+ includes_randomness = True
48
+ includes_timestamp = True
49
+ if option.endswith("uuid"):
50
+ value = str(ulid.to_uuid())
51
+ elif option.endswith("str"):
52
+ value = str(ulid)
53
+ elif option.endswith("hex"):
54
+ value = ulid.hex
55
+ elif option.endswith("int"):
56
+ value = str(int(ulid))
57
+ elif option.endswith("timestamp"):
58
+ value = str(ulid.timestamp)
59
+ includes_randomness = False
60
+ elif option.endswith("datetime"):
61
+ value = ulid.datetime.isoformat()
62
+ includes_randomness = False
63
+ else:
64
+ includes_timestamp = False
65
+ includes_randomness = False
66
+
67
+ argv = ["build"]
68
+ if option:
69
+ argv += [f"--{option}", value]
70
+ output = cli.main(argv)
71
+
72
+ if includes_randomness and includes_timestamp:
73
+ assert output == str(ulid)
74
+
75
+ ulid_out = ULID.from_str(output)
76
+ if includes_timestamp:
77
+ assert ulid_out.datetime == ulid.datetime
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import time
2
3
  import uuid
3
4
  from collections.abc import Callable
@@ -8,6 +9,8 @@ from typing import Union
8
9
 
9
10
  import pytest
10
11
  from freezegun import freeze_time
12
+ from pydantic import BaseModel
13
+ from pydantic import ValidationError
11
14
 
12
15
  from ulid import base32
13
16
  from ulid import constants
@@ -115,7 +118,8 @@ def test_hash() -> None:
115
118
  @freeze_time()
116
119
  def test_ulid_from_time() -> None:
117
120
  ulid1 = ULID.from_timestamp(time.time())
118
- ulid2 = ULID.from_datetime(utcnow())
121
+ ulid2 = ULID.from_timestamp(time.time_ns() // 1000000)
122
+ ulid3 = ULID.from_datetime(utcnow())
119
123
 
120
124
  now = utcnow()
121
125
  t = time.time()
@@ -126,6 +130,9 @@ def test_ulid_from_time() -> None:
126
130
  assert ulid2.timestamp == pytest.approx(t)
127
131
  datetimes_almost_equal(ulid2.datetime, now)
128
132
 
133
+ assert ulid2.timestamp == pytest.approx(t)
134
+ datetimes_almost_equal(ulid3.datetime, now)
135
+
129
136
 
130
137
  @freeze_time()
131
138
  def test_ulid_from_timestamp() -> None:
@@ -155,3 +162,29 @@ Params = Union[bytes, str, int, float]
155
162
  def test_ulid_invalid_input(constructor: Callable[[Params], ULID], value: Params) -> None:
156
163
  with pytest.raises(ValueError): # noqa: PT011
157
164
  constructor(value)
165
+
166
+
167
+ def test_pydantic_protocol() -> None:
168
+ ulid = ULID()
169
+
170
+ class Model(BaseModel):
171
+ ulid: ULID
172
+
173
+ for value in [ulid, str(ulid), int(ulid), bytes(ulid)]:
174
+ model = Model(ulid=value)
175
+ assert isinstance(model.ulid, ULID)
176
+ assert model.ulid == ulid
177
+
178
+ for value in [b"not-enough", "not-enough"]:
179
+ with pytest.raises(ValidationError):
180
+ Model(ulid=value)
181
+
182
+ model_dict = model.model_dump()
183
+ ulid_from_dict = model_dict["ulid"]
184
+ assert ulid_from_dict == ulid
185
+ assert isinstance(ulid_from_dict, ULID)
186
+ assert Model(**model_dict) == model
187
+
188
+ model_json = model.model_dump_json()
189
+ assert isinstance(json.loads(model_json)["ulid"], str)
190
+ assert Model.model_validate_json(model_json) == model
@@ -9,15 +9,21 @@ from datetime import datetime
9
9
  from datetime import timezone
10
10
  from typing import Any
11
11
  from typing import Generic
12
+ from typing import TYPE_CHECKING
12
13
  from typing import TypeVar
13
14
 
14
15
  from ulid import base32
15
16
  from ulid import constants
16
17
 
17
18
 
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from pydantic import GetCoreSchemaHandler
21
+ from pydantic import ValidatorFunctionWrapHandler
22
+ from pydantic_core import CoreSchema
23
+
18
24
  try:
19
25
  from importlib.metadata import version
20
- except ImportError:
26
+ except ImportError: # pragma: no cover
21
27
  from importlib_metadata import version # type: ignore
22
28
 
23
29
 
@@ -44,6 +50,9 @@ class validate_type(Generic[T]): # noqa: N801
44
50
  return wrapped
45
51
 
46
52
 
53
+ U = TypeVar("U", bound="ULID")
54
+
55
+
47
56
  @functools.total_ordering
48
57
  class ULID:
49
58
  """The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
@@ -67,11 +76,13 @@ class ULID:
67
76
  def __init__(self, value: bytes | None = None) -> None:
68
77
  if value is not None and len(value) != constants.BYTES_LEN:
69
78
  raise ValueError("ULID has to be exactly 16 bytes long.")
70
- self.bytes: bytes = value or ULID.from_timestamp(time.time()).bytes
79
+ self.bytes: bytes = (
80
+ value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
81
+ )
71
82
 
72
83
  @classmethod
73
84
  @validate_type(datetime)
74
- def from_datetime(cls, value: datetime) -> ULID:
85
+ def from_datetime(cls: type[U], value: datetime) -> U:
75
86
  """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
76
87
  `ULID` will be set to the corresponding timestamp of the datetime.
77
88
 
@@ -85,7 +96,7 @@ class ULID:
85
96
 
86
97
  @classmethod
87
98
  @validate_type(int, float)
88
- def from_timestamp(cls, value: int | float) -> ULID:
99
+ def from_timestamp(cls: type[U], value: int | float) -> U:
89
100
  """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
90
101
  `float` representing the time in seconds (as it would be returned by :func:`time.time()`)
91
102
  or an `int` in milliseconds.
@@ -104,7 +115,7 @@ class ULID:
104
115
 
105
116
  @classmethod
106
117
  @validate_type(uuid.UUID)
107
- def from_uuid(cls, value: uuid.UUID) -> ULID:
118
+ def from_uuid(cls: type[U], value: uuid.UUID) -> U:
108
119
  """Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
109
120
  random in that case.
110
121
 
@@ -118,25 +129,25 @@ class ULID:
118
129
 
119
130
  @classmethod
120
131
  @validate_type(bytes)
121
- def from_bytes(cls, bytes_: bytes) -> ULID:
132
+ def from_bytes(cls: type[U], bytes_: bytes) -> U:
122
133
  """Create a new :class:`ULID`-object from sequence of 16 bytes."""
123
134
  return cls(bytes_)
124
135
 
125
136
  @classmethod
126
137
  @validate_type(str)
127
- def from_hex(cls, value: str) -> ULID:
138
+ def from_hex(cls: type[U], value: str) -> U:
128
139
  """Create a new :class:`ULID`-object from 32 character string of hex values."""
129
140
  return cls.from_bytes(bytes.fromhex(value))
130
141
 
131
142
  @classmethod
132
143
  @validate_type(str)
133
- def from_str(cls, string: str) -> ULID:
144
+ def from_str(cls: type[U], string: str) -> U:
134
145
  """Create a new :class:`ULID`-object from a 26 char long string representation."""
135
146
  return cls(base32.decode(string))
136
147
 
137
148
  @classmethod
138
149
  @validate_type(int)
139
- def from_int(cls, value: int) -> ULID:
150
+ def from_int(cls: type[U], value: int) -> U:
140
151
  """Create a new :class:`ULID`-object from an `int`."""
141
152
  return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
142
153
 
@@ -209,6 +220,10 @@ class ULID:
209
220
  """Encode this object as an integer."""
210
221
  return int.from_bytes(self.bytes, byteorder="big")
211
222
 
223
+ def __bytes__(self) -> bytes:
224
+ """Encode this object as byte sequence."""
225
+ return self.bytes
226
+
212
227
  def __lt__(self, other: Any) -> bool:
213
228
  if isinstance(other, ULID):
214
229
  return self.bytes < other.bytes
@@ -233,3 +248,37 @@ class ULID:
233
248
 
234
249
  def __hash__(self) -> int:
235
250
  return hash(self.bytes)
251
+
252
+ @classmethod
253
+ def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
254
+ from pydantic_core import core_schema
255
+
256
+ return core_schema.no_info_wrap_validator_function(
257
+ cls._pydantic_validate,
258
+ core_schema.union_schema(
259
+ [
260
+ core_schema.is_instance_schema(ULID),
261
+ core_schema.no_info_plain_validator_function(ULID),
262
+ ]
263
+ ),
264
+ serialization=core_schema.to_string_ser_schema(
265
+ when_used="json-unless-none",
266
+ ),
267
+ )
268
+
269
+ @classmethod
270
+ def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
271
+ from pydantic_core import PydanticCustomError
272
+
273
+ try:
274
+ if isinstance(value, int):
275
+ ulid = cls.from_int(value)
276
+ elif isinstance(value, str):
277
+ ulid = cls.from_str(value)
278
+ elif isinstance(value, ULID):
279
+ ulid = value
280
+ else:
281
+ ulid = cls.from_bytes(value)
282
+ except ValueError as err:
283
+ raise PydanticCustomError("ulid_format", "Unrecognized format") from err
284
+ return handler(ulid)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import shutil
3
5
  import sys
@@ -7,7 +9,6 @@ from collections.abc import Sequence
7
9
  from datetime import datetime
8
10
  from functools import partial
9
11
  from typing import Any
10
- from typing import Optional
11
12
  from uuid import UUID
12
13
 
13
14
  import ulid
@@ -92,12 +93,12 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
92
93
  return parser
93
94
 
94
95
 
95
- def main(argv: Sequence[str], prog: str | None = None) -> None:
96
+ def main(argv: Sequence[str], prog: str | None = None) -> str:
96
97
  args = make_parser(prog).parse_args(argv)
97
- args.func(args)
98
+ return args.func(args)
98
99
 
99
100
 
100
- def from_value_or_stdin(value: str, convert: Optional[Callable[[str], Any]] = None) -> Any:
101
+ def from_value_or_stdin(value: str, convert: Callable[[str], Any] | None = None) -> Any:
101
102
  value = sys.stdin.readline().strip() if value == "-" else value
102
103
  if convert is not None:
103
104
  return convert(value)
@@ -111,7 +112,7 @@ def parse_numeric(s: str) -> int | float:
111
112
  return float(s)
112
113
 
113
114
 
114
- def build(args: argparse.Namespace) -> None:
115
+ def build(args: argparse.Namespace) -> str:
115
116
  ulid: ULID
116
117
  if args.from_int is not None:
117
118
  ulid = ULID.from_int(from_value_or_stdin(args.from_int, int))
@@ -127,40 +128,38 @@ def build(args: argparse.Namespace) -> None:
127
128
  ulid = ULID.from_uuid(from_value_or_stdin(args.from_uuid, UUID))
128
129
  else:
129
130
  ulid = ULID()
130
- print(ulid)
131
+ return str(ulid)
131
132
 
132
133
 
133
- def show(args: argparse.Namespace) -> None:
134
+ def show(args: argparse.Namespace) -> str:
134
135
  ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid))
135
136
  if args.uuid:
136
- print(ulid.to_uuid())
137
+ return str(ulid.to_uuid())
137
138
  elif args.uuid4:
138
- print(ulid.to_uuid4())
139
+ return str(ulid.to_uuid4())
139
140
  elif args.hex:
140
- print(ulid.hex)
141
+ return ulid.hex
141
142
  elif args.int:
142
- print(int(ulid))
143
+ return str(int(ulid))
143
144
  elif args.timestamp:
144
- print(ulid.timestamp)
145
+ return str(ulid.timestamp)
145
146
  elif args.datetime:
146
- print(ulid.datetime)
147
+ return ulid.datetime.isoformat()
147
148
  else:
148
- print(
149
- textwrap.dedent(
150
- f"""
151
- ULID: {ulid!s}
152
- Hex: {ulid.hex}
153
- Int: {int(ulid)}
154
- Timestamp: {ulid.timestamp}
155
- Datetime: {ulid.datetime}
156
- """
157
- ).strip()
158
- )
149
+ return textwrap.dedent(
150
+ f"""
151
+ ULID: {ulid!s}
152
+ Hex: {ulid.hex}
153
+ Int: {int(ulid)}
154
+ Timestamp: {ulid.timestamp}
155
+ Datetime: {ulid.datetime.isoformat()}
156
+ """
157
+ ).strip()
159
158
 
160
159
 
161
- def entrypoint() -> None:
162
- main(sys.argv[1:])
160
+ def entrypoint() -> None: # pragma: no cover
161
+ print(main(sys.argv[1:]))
163
162
 
164
163
 
165
- if __name__ == "__main__":
164
+ if __name__ == "__main__": # pragma: no cover
166
165
  main(sys.argv[1:], "python -m ulid")
@@ -1,4 +1,5 @@
1
1
  MILLISECS_IN_SECS = 1000
2
+ NANOSECS_IN_MILLISECS = 1000000
2
3
 
3
4
  TIMESTAMP_LEN = 6
4
5
  RANDOMNESS_LEN = 10
@@ -1,18 +0,0 @@
1
- repos:
2
- - repo: https://github.com/psf/black
3
- rev: 23.9.1
4
- hooks:
5
- - id: black
6
- - repo: https://github.com/timothycrosley/isort
7
- rev: 5.12.0
8
- hooks:
9
- - id: isort
10
- - repo: https://github.com/astral-sh/ruff-pre-commit
11
- # Ruff version.
12
- rev: v0.0.290
13
- hooks:
14
- - id: ruff
15
- - repo: https://github.com/pycqa/doc8
16
- rev: v1.1.1
17
- hooks:
18
- - id: doc8
@@ -1 +0,0 @@
1
- .. include:: ../CHANGELOG.rst
@@ -1 +0,0 @@
1
- furo
File without changes
File without changes
File without changes
File without changes