python-ulid 2.1.0__tar.gz → 2.3.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.1.0 → python_ulid-2.3.0}/.github/workflows/lint-and-test.yml +19 -6
  2. {python_ulid-2.1.0 → python_ulid-2.3.0}/.github/workflows/publish.yml +1 -1
  3. {python_ulid-2.1.0 → python_ulid-2.3.0}/.gitignore +1 -1
  4. python_ulid-2.3.0/.pre-commit-config.yaml +10 -0
  5. {python_ulid-2.1.0 → python_ulid-2.3.0}/.readthedocs.yml +3 -3
  6. {python_ulid-2.1.0 → python_ulid-2.3.0}/CHANGELOG.rst +32 -0
  7. {python_ulid-2.1.0 → python_ulid-2.3.0}/PKG-INFO +71 -8
  8. {python_ulid-2.1.0 → python_ulid-2.3.0}/README.rst +66 -6
  9. {python_ulid-2.1.0 → python_ulid-2.3.0}/docs/Makefile +2 -2
  10. python_ulid-2.3.0/docs/requirements.txt +2 -0
  11. python_ulid-2.3.0/docs/source/changelog.rst +1 -0
  12. {python_ulid-2.1.0/docs → python_ulid-2.3.0/docs/source}/conf.py +5 -11
  13. {python_ulid-2.1.0/docs → python_ulid-2.3.0/docs/source}/index.rst +8 -4
  14. {python_ulid-2.1.0 → python_ulid-2.3.0}/hatch.toml +13 -9
  15. {python_ulid-2.1.0 → python_ulid-2.3.0}/pyproject.toml +11 -10
  16. python_ulid-2.3.0/tests/test_cli.py +77 -0
  17. {python_ulid-2.1.0 → python_ulid-2.3.0}/tests/test_ulid.py +23 -1
  18. {python_ulid-2.1.0 → python_ulid-2.3.0}/ulid/__init__.py +55 -9
  19. {python_ulid-2.1.0 → python_ulid-2.3.0}/ulid/__main__.py +47 -42
  20. {python_ulid-2.1.0 → python_ulid-2.3.0}/ulid/constants.py +1 -0
  21. python_ulid-2.1.0/.pre-commit-config.yaml +0 -18
  22. python_ulid-2.1.0/docs/changelog.rst +0 -1
  23. python_ulid-2.1.0/docs/requirements.txt +0 -1
  24. {python_ulid-2.1.0 → python_ulid-2.3.0}/LICENSE +0 -0
  25. {python_ulid-2.1.0/docs → python_ulid-2.3.0/docs/source}/api.rst +0 -0
  26. {python_ulid-2.1.0 → python_ulid-2.3.0}/logo.png +0 -0
  27. {python_ulid-2.1.0 → python_ulid-2.3.0}/tests/__init__.py +0 -0
  28. {python_ulid-2.1.0 → python_ulid-2.3.0}/tests/test_base32.py +0 -0
  29. {python_ulid-2.1.0 → python_ulid-2.3.0}/ulid/base32.py +0 -0
  30. {python_ulid-2.1.0 → python_ulid-2.3.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,36 @@ Changelog
5
5
 
6
6
  Versions follow `Semantic Versioning <http://www.semver.org>`_
7
7
 
8
+ `2.3.0`_ - 2023-09-21
9
+ ---------------------
10
+
11
+ Added
12
+ ~~~~~
13
+ * :class:`.ULID` objects can now be converted to bytes with ``bytes(ulid)``.
14
+ * The Pydantic v2 protocol is now supported, so that the :class:`.ULID` class can be directly used
15
+ as type annotations in `Pydantic models <https://docs.pydantic.dev/latest/concepts/models/#basic-model-usage>`_
16
+
17
+ Changed
18
+ ~~~~~~~
19
+ * The type annotations have been adapted, so that the classmethod constructors properly reflect the
20
+ type for :class:`.ULID` subclasses. Thanks to `@johnpaulett <https://github.com/johnpaulett>`_
21
+
22
+
23
+ `2.2.0`_ - 2023-09-21
24
+ ---------------------
25
+
26
+ Added
27
+ ~~~~~
28
+ * Added a new flag ``--uuid4`` to the CLI ``show`` command, that converts the provided ``ULID``
29
+ into an RFC 4122 compliant ``UUID``.
30
+ * The `ulid build` command allows the use of the special value ``-`` for all options to read its
31
+ inputs from ``stdin``. E.g.
32
+
33
+ .. code-block:: bash
34
+
35
+ $ date --iso-8601 | python -m ulid build --from-datetime -
36
+ 01HAT9PVR02T3S13XB48S7GEHE
37
+
8
38
  `2.1.0`_ - 2023-09-21
9
39
  ---------------------
10
40
 
@@ -113,6 +143,8 @@ Changed
113
143
  * The package now has no external dependencies.
114
144
  * The test-coverage has been raised to 100%.
115
145
 
146
+ .. _2.3.0: https://github.com/mdomke/python-ulid/compare/2.2.0...2.3.0
147
+ .. _2.2.0: https://github.com/mdomke/python-ulid/compare/2.1.0...2.2.0
116
148
  .. _2.1.0: https://github.com/mdomke/python-ulid/compare/2.0.0...2.1.0
117
149
  .. _2.0.0: https://github.com/mdomke/python-ulid/compare/1.1.0...2.0.0
118
150
  .. _1.1.0: https://github.com/mdomke/python-ulid/compare/1.0.3...1.1.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-ulid
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Universally unique lexicographically sortable identifier
5
5
  Author-email: Martin Domke <mail@martindomke.net>
6
6
  License-Expression: MIT
@@ -15,8 +15,11 @@ Classifier: Programming Language :: Python
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Topic :: Software Development :: Libraries
19
20
  Requires-Python: >=3.9
21
+ Provides-Extra: pydantic
22
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
20
23
  Description-Content-Type: text/x-rst
21
24
 
22
25
 
@@ -57,6 +60,12 @@ Use ``pip`` to install the library
57
60
 
58
61
  $ pip install python-ulid
59
62
 
63
+ to include Pydantic support install the optional dependency like so
64
+
65
+ .. code-block:: bash
66
+
67
+ $ pip install python-ulid[pydantic]
68
+
60
69
  .. installation-end
61
70
 
62
71
  .. usage-begin
@@ -82,8 +91,8 @@ or use one of the named constructors
82
91
  >>> ULID.from_datetime(datetime.datetime.now())
83
92
  ULID(01E75J2XBK390V2XRH44EHC10X)
84
93
 
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:
94
+ There are several options for encoding the ``ULID`` object
95
+ (e.g. string, hex, int, bytes, UUID):
87
96
 
88
97
  .. code-block:: pycon
89
98
 
@@ -91,22 +100,54 @@ as well as to access the timestamp attribute in different formats:
91
100
  '01BTGNYV6HRNK8K8VKZASZCFPE'
92
101
  >>> ulid.hex
93
102
  '015ea15f6cd1c56689a373fab3f63ece'
103
+ >>> int(ulid)
104
+ 1820576928786795198723644692628913870
105
+ >>> bytes(ulid)
106
+ b'\x01^\xa1_l\xd1\xc5f\x89\xa3s\xfa\xb3\xf6>\xce'
107
+ >>> ulid.to_uuid()
108
+ UUID('015ea15f-6cd1-c566-89a3-73fab3f63ece')
109
+
110
+ It is also possible to directly access the timestamp component of a ``ULID``,
111
+ either in UNIX epoch or as ``datetime.datetime``
112
+
113
+ .. code-block:: pycon
114
+
94
115
  >>> ulid.timestamp
95
116
  1505945939.153
96
117
  >>> ulid.datetime
97
118
  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
119
 
101
120
  .. usage-end
102
121
 
122
+ .. pydantic-begin
123
+
124
+ Pydantic integration
125
+ ---------------------
126
+
127
+ The ``ULID`` class can be directly used for the popular data validation library
128
+ `Pydantic <https://docs.pydantic.dev/latest/>`_ like so
129
+
130
+ .. code-block:: python
131
+
132
+ from pydantic import BaseModel
133
+ from ulid import ULID
134
+
135
+
136
+ class Model(BaseModel):
137
+ ulid: ULID
138
+
139
+ model = Model(ulid="DX89370400440532013000") # OK
140
+ model = Model(ulid="not-a-ulid") # Raises ValidationError
141
+
142
+ .. pydantic-end
143
+
103
144
  .. cli-begin
104
145
 
105
146
  Command line interface
106
147
  -----------------------
107
148
 
108
149
  The package comes with a CLI interface that can be invoked either by the script name
109
- `ulid` or as python module `python -m ulid`. The CLI allows you to generate, inspect
150
+ ``ulid`` or as python module ``python -m ulid``. The CLI allows you to generate, inspect
110
151
  and convert ULIDs, e.g.
111
152
 
112
153
  .. code-block:: bash
@@ -124,8 +165,26 @@ and convert ULIDs, e.g.
124
165
  Timestamp: 1695219822.248
125
166
  Datetime: 2023-09-20 14:23:42.248000+00:00
126
167
 
168
+ There are several flags to select specific output formats for the ``show`` command, e.g.
169
+
170
+
171
+ .. code-block:: bash
172
+
173
+ $ ulid show --datetime 01HASFKBN8SKZTSVVS03K5AMMS
174
+ 2023-09-20 14:23:42.248000+00:00
175
+
176
+ The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
177
+
178
+ .. code-block:: bash
179
+
127
180
  $ echo 01HASFKBN8SKZTSVVS03K5AMMS | ulid show --uuid -
128
- 018ab2f9-aea8-ccff-acef-7900e6555299
181
+ 018ab2f9-aea8-4cff-acef-7900e6555299
182
+
183
+ $ date --iso-8601 | python -m ulid build --from-datetime -
184
+ 01HAT9PVR02T3S13XB48S7GEHE
185
+
186
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
187
+ (e.g. ``ulid show --help``).
129
188
 
130
189
  .. cli-end
131
190
 
@@ -135,3 +194,7 @@ Other implementations
135
194
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
136
195
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
137
196
  * `mdipierro/ulid <https://github.com/mdipierro/ulid>`_
197
+ * `oklog/ulid <https://github.com/oklog/ulid>`_
198
+ * `ulid/javascript <https://github.com/ulid/javascript>`_
199
+ * `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
200
+ * `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,22 +109,54 @@ 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
118
156
  -----------------------
119
157
 
120
158
  The package comes with a CLI interface that can be invoked either by the script name
121
- `ulid` or as python module `python -m ulid`. The CLI allows you to generate, inspect
159
+ ``ulid`` or as python module ``python -m ulid``. The CLI allows you to generate, inspect
122
160
  and convert ULIDs, e.g.
123
161
 
124
162
  .. code-block:: bash
@@ -136,8 +174,26 @@ 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
+
185
+ The special character ``-`` allows to read values from ``stdin`` so that they can be piped. E.g.
186
+
187
+ .. code-block:: bash
188
+
139
189
  $ echo 01HASFKBN8SKZTSVVS03K5AMMS | ulid show --uuid -
140
- 018ab2f9-aea8-ccff-acef-7900e6555299
190
+ 018ab2f9-aea8-4cff-acef-7900e6555299
191
+
192
+ $ date --iso-8601 | python -m ulid build --from-datetime -
193
+ 01HAT9PVR02T3S13XB48S7GEHE
194
+
195
+ For a full overview of flags for the ``build`` and ``show`` commands use the ``--help`` option
196
+ (e.g. ``ulid show --help``).
141
197
 
142
198
  .. cli-end
143
199
 
@@ -147,3 +203,7 @@ Other implementations
147
203
  * `ahawker/ulid <https://github.com/ahawker/ulid>`_
148
204
  * `valohai/ulid2 <https://github.com/valohai/ulid2>`_
149
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,15 @@ 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.optional-dependencies]
30
+ pydantic = [
31
+ "pydantic>=2.0"
32
+ ]
33
+
28
34
  [project.scripts]
29
35
  ulid = "ulid.__main__:entrypoint"
30
36
 
@@ -42,6 +48,8 @@ line-length = 100
42
48
  [tool.ruff]
43
49
  target-version = "py39"
44
50
  line-length = 100
51
+
52
+ [tool.ruff.lint]
45
53
  select = ["A", "B", "C", "C4", "E", "F", "I", "N", "PT", "Q", "RUF", "S", "SIM", "T10", "UP", "W", "YTT"]
46
54
  fixable = ["RUF100", "I001"]
47
55
  ignore = [
@@ -50,21 +58,14 @@ ignore = [
50
58
  "A003", # Allow shadowing bultins on classes
51
59
  ]
52
60
 
53
- [tool.ruff.mccabe]
61
+ [tool.ruff.lint.mccabe]
54
62
  max-complexity = 15
55
63
 
56
- [tool.ruff.isort]
64
+ [tool.ruff.lint.isort]
57
65
  force-single-line = true
58
66
  lines-after-imports = 2
59
67
  order-by-type = false
60
68
 
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
69
  [tool.coverage.run]
69
70
  branch = true
70
71
  parallel = true
@@ -78,4 +79,4 @@ content-type = "text/x-rst"
78
79
 
79
80
  [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
80
81
  path = "README.rst"
81
- start-after = ".. teaser-begin"
82
+ 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
@@ -8,6 +8,8 @@ from typing import Union
8
8
 
9
9
  import pytest
10
10
  from freezegun import freeze_time
11
+ from pydantic import BaseModel
12
+ from pydantic import ValidationError
11
13
 
12
14
  from ulid import base32
13
15
  from ulid import constants
@@ -115,7 +117,8 @@ def test_hash() -> None:
115
117
  @freeze_time()
116
118
  def test_ulid_from_time() -> None:
117
119
  ulid1 = ULID.from_timestamp(time.time())
118
- ulid2 = ULID.from_datetime(utcnow())
120
+ ulid2 = ULID.from_timestamp(time.time_ns() // 1000000)
121
+ ulid3 = ULID.from_datetime(utcnow())
119
122
 
120
123
  now = utcnow()
121
124
  t = time.time()
@@ -126,6 +129,9 @@ def test_ulid_from_time() -> None:
126
129
  assert ulid2.timestamp == pytest.approx(t)
127
130
  datetimes_almost_equal(ulid2.datetime, now)
128
131
 
132
+ assert ulid2.timestamp == pytest.approx(t)
133
+ datetimes_almost_equal(ulid3.datetime, now)
134
+
129
135
 
130
136
  @freeze_time()
131
137
  def test_ulid_from_timestamp() -> None:
@@ -155,3 +161,19 @@ Params = Union[bytes, str, int, float]
155
161
  def test_ulid_invalid_input(constructor: Callable[[Params], ULID], value: Params) -> None:
156
162
  with pytest.raises(ValueError): # noqa: PT011
157
163
  constructor(value)
164
+
165
+
166
+ def test_pydantic_protocol() -> None:
167
+ ulid = ULID()
168
+
169
+ class Model(BaseModel):
170
+ ulid: ULID
171
+
172
+ for value in [ulid, str(ulid), int(ulid), bytes(ulid)]:
173
+ model = Model(ulid=value)
174
+ assert isinstance(model.ulid, ULID)
175
+ assert model.ulid == ulid
176
+
177
+ for value in [b"not-enough", "not-enough"]:
178
+ with pytest.raises(ValidationError):
179
+ Model(ulid=value)
@@ -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,34 @@ 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
+ )
265
+
266
+ @classmethod
267
+ def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
268
+ from pydantic_core import PydanticCustomError
269
+
270
+ try:
271
+ if isinstance(value, int):
272
+ ulid = cls.from_int(value)
273
+ elif isinstance(value, str):
274
+ ulid = cls.from_str(value)
275
+ elif isinstance(value, ULID):
276
+ ulid = value
277
+ else:
278
+ ulid = cls.from_bytes(value)
279
+ except ValueError as err:
280
+ raise PydanticCustomError("ulid_format", "Unrecognized format") from err
281
+ return handler(ulid)
@@ -1,10 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import shutil
3
5
  import sys
4
6
  import textwrap
7
+ from collections.abc import Callable
5
8
  from collections.abc import Sequence
6
9
  from datetime import datetime
7
10
  from functools import partial
11
+ from typing import Any
8
12
  from uuid import UUID
9
13
 
10
14
  import ulid
@@ -47,37 +51,31 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
47
51
  )
48
52
  b.add_argument(
49
53
  "--from-int",
50
- type=int,
51
54
  metavar="<int>",
52
55
  help="create from integer",
53
56
  )
54
57
  b.add_argument(
55
58
  "--from-hex",
56
- type=str,
57
59
  metavar="<str>",
58
60
  help="create from 32 character hex value",
59
61
  )
60
62
  b.add_argument(
61
63
  "--from-str",
62
- type=str,
63
64
  metavar="<str>",
64
65
  help="create from base32 encoded string of length 26",
65
66
  )
66
67
  b.add_argument(
67
68
  "--from-timestamp",
68
- type=parse_numeric,
69
69
  metavar="<int|float>",
70
70
  help="create from timestamp either as float in secs or int as millis",
71
71
  )
72
72
  b.add_argument(
73
73
  "--from-datetime",
74
- type=datetime.fromisoformat,
75
74
  metavar="<iso8601>",
76
75
  help="create from datetime. The timestamp part of the ULID will be taken from the datetime",
77
76
  )
78
77
  b.add_argument(
79
78
  "--from-uuid",
80
- type=UUID,
81
79
  metavar="<uuid>",
82
80
  help="create from given UUID. The timestamp part will be random.",
83
81
  )
@@ -85,7 +83,8 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
85
83
 
86
84
  s = subparsers.add_parser("show", help="show properties of a ULID")
87
85
  s.add_argument("ulid", help="the ULID to inspect. The special value - reads from stdin")
88
- s.add_argument("--uuid", action="store_true", help="convert to UUID")
86
+ s.add_argument("--uuid", action="store_true", help="convert to fully random UUID")
87
+ s.add_argument("--uuid4", action="store_true", help="convert to RFC 4122 compliant UUIDv4")
89
88
  s.add_argument("--hex", action="store_true", help="convert to hex")
90
89
  s.add_argument("--int", action="store_true", help="convert to int")
91
90
  s.add_argument("--timestamp", "--ts", action="store_true", help="show timestamp")
@@ -94,6 +93,18 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
94
93
  return parser
95
94
 
96
95
 
96
+ def main(argv: Sequence[str], prog: str | None = None) -> str:
97
+ args = make_parser(prog).parse_args(argv)
98
+ return args.func(args)
99
+
100
+
101
+ def from_value_or_stdin(value: str, convert: Callable[[str], Any] | None = None) -> Any:
102
+ value = sys.stdin.readline().strip() if value == "-" else value
103
+ if convert is not None:
104
+ return convert(value)
105
+ return value
106
+
107
+
97
108
  def parse_numeric(s: str) -> int | float:
98
109
  try:
99
110
  return int(s)
@@ -101,60 +112,54 @@ def parse_numeric(s: str) -> int | float:
101
112
  return float(s)
102
113
 
103
114
 
104
- def main(argv: Sequence[str], prog: str | None = None) -> None:
105
- args = make_parser(prog).parse_args(argv)
106
- args.func(args)
107
-
108
-
109
- def build(args: argparse.Namespace) -> None:
115
+ def build(args: argparse.Namespace) -> str:
110
116
  ulid: ULID
111
117
  if args.from_int is not None:
112
- ulid = ULID.from_int(args.from_int)
118
+ ulid = ULID.from_int(from_value_or_stdin(args.from_int, int))
113
119
  elif args.from_hex is not None:
114
- ulid = ULID.from_hex(args.from_hex)
120
+ ulid = ULID.from_hex(from_value_or_stdin(args.from_hex))
115
121
  elif args.from_str is not None:
116
- ulid = ULID.from_str(args.from_str)
122
+ ulid = ULID.from_str(from_value_or_stdin(args.from_str))
117
123
  elif args.from_timestamp is not None:
118
- ulid = ULID.from_timestamp(args.from_timestamp)
124
+ ulid = ULID.from_timestamp(from_value_or_stdin(args.from_timestamp, parse_numeric))
119
125
  elif args.from_datetime is not None:
120
- ulid = ULID.from_datetime(args.from_datetime)
126
+ ulid = ULID.from_datetime(from_value_or_stdin(args.from_datetime, datetime.fromisoformat))
121
127
  elif args.from_uuid is not None:
122
- ulid = ULID.from_uuid(args.from_uuid)
128
+ ulid = ULID.from_uuid(from_value_or_stdin(args.from_uuid, UUID))
123
129
  else:
124
130
  ulid = ULID()
125
- print(ulid)
131
+ return str(ulid)
126
132
 
127
133
 
128
- def show(args: argparse.Namespace) -> None:
129
- value = sys.stdin.readline().strip() if args.ulid == "-" else args.ulid
130
- ulid: ULID = ULID.from_str(value)
134
+ def show(args: argparse.Namespace) -> str:
135
+ ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid))
131
136
  if args.uuid:
132
- print(ulid.to_uuid())
137
+ return str(ulid.to_uuid())
138
+ elif args.uuid4:
139
+ return str(ulid.to_uuid4())
133
140
  elif args.hex:
134
- print(ulid.hex)
141
+ return ulid.hex
135
142
  elif args.int:
136
- print(int(ulid))
143
+ return str(int(ulid))
137
144
  elif args.timestamp:
138
- print(ulid.timestamp)
145
+ return str(ulid.timestamp)
139
146
  elif args.datetime:
140
- print(ulid.datetime)
147
+ return ulid.datetime.isoformat()
141
148
  else:
142
- print(
143
- textwrap.dedent(
144
- f"""
145
- ULID: {ulid!s}
146
- Hex: {ulid.hex}
147
- Int: {int(ulid)}
148
- Timestamp: {ulid.timestamp}
149
- Datetime: {ulid.datetime}
150
- """
151
- ).strip()
152
- )
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()
153
158
 
154
159
 
155
- def entrypoint() -> None:
156
- main(sys.argv[1:])
160
+ def entrypoint() -> None: # pragma: no cover
161
+ print(main(sys.argv[1:]))
157
162
 
158
163
 
159
- if __name__ == "__main__":
164
+ if __name__ == "__main__": # pragma: no cover
160
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