batutils 2.0.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.
- batutils-2.0.0/LICENSE +22 -0
- batutils-2.0.0/PKG-INFO +125 -0
- batutils-2.0.0/README.md +74 -0
- batutils-2.0.0/batutils/__init__.py +5 -0
- batutils-2.0.0/batutils/_common/__init__.py +6 -0
- batutils-2.0.0/batutils/_common/_decode.py +40 -0
- batutils-2.0.0/batutils/dtu/__init__.py +19 -0
- batutils-2.0.0/batutils/dtu/casting/__init__.py +5 -0
- batutils-2.0.0/batutils/dtu/casting/_any.py +136 -0
- batutils-2.0.0/batutils/dtu/casting/_filetime.py +36 -0
- batutils-2.0.0/batutils/dtu/casting/_instant.py +31 -0
- batutils-2.0.0/batutils/dtu/casting/_string.py +81 -0
- batutils-2.0.0/batutils/dtu/casting/_timestamp.py +50 -0
- batutils-2.0.0/batutils/dtu/formatting/__init__.py +2 -0
- batutils-2.0.0/batutils/dtu/formatting/_delta.py +62 -0
- batutils-2.0.0/batutils/dtu/formatting/_serialize.py +26 -0
- batutils-2.0.0/batutils/dtu/parsers/__init__.py +2 -0
- batutils-2.0.0/batutils/dtu/parsers/_string.py +44 -0
- batutils-2.0.0/batutils/dtu/parsers/_timedelta.py +161 -0
- batutils-2.0.0/batutils/dtu/shortcuts/__init__.py +3 -0
- batutils-2.0.0/batutils/dtu/shortcuts/_aliases.py +29 -0
- batutils-2.0.0/batutils/dtu/shortcuts/_checkers.py +21 -0
- batutils-2.0.0/batutils/dtu/shortcuts/_finders.py +80 -0
- batutils-2.0.0/batutils/dtu/structs/__init__.py +5 -0
- batutils-2.0.0/batutils/dtu/structs/_base.py +5 -0
- batutils-2.0.0/batutils/dtu/structs/_deltasplit.py +278 -0
- batutils-2.0.0/batutils/dtu/structs/_types.py +24 -0
- batutils-2.0.0/batutils/dtu/structs/aliases.py +21 -0
- batutils-2.0.0/batutils/dtu/structs/consts.py +35 -0
- batutils-2.0.0/batutils/structs/__init__.py +7 -0
- batutils-2.0.0/batutils/structs/_base.py +21 -0
- batutils-2.0.0/batutils/tpu/__init__.py +0 -0
- batutils-2.0.0/batutils/tpu/casting/__init__.py +9 -0
- batutils-2.0.0/batutils/tpu/casting/_currency.py +52 -0
- batutils-2.0.0/batutils/tpu/casting/_evals.py +247 -0
- batutils-2.0.0/batutils/tpu/casting/_ip.py +58 -0
- batutils-2.0.0/batutils/tpu/casting/_mask.py +34 -0
- batutils-2.0.0/batutils/tpu/casting/_params.py +108 -0
- batutils-2.0.0/batutils/tpu/casting/_serialize.py +20 -0
- batutils-2.0.0/batutils/tpu/comparison/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/comparison/_mixin.py +30 -0
- batutils-2.0.0/batutils/tpu/insp/__init__.py +2 -0
- batutils-2.0.0/batutils/tpu/insp/_insp_old.py +347 -0
- batutils-2.0.0/batutils/tpu/insp/_typecheck.py +89 -0
- batutils-2.0.0/batutils/tpu/iters/__init__.py +4 -0
- batutils-2.0.0/batutils/tpu/iters/_attrs.py +84 -0
- batutils-2.0.0/batutils/tpu/iters/_dicts.py +130 -0
- batutils-2.0.0/batutils/tpu/iters/_group.py +50 -0
- batutils-2.0.0/batutils/tpu/iters/_lists.py +178 -0
- batutils-2.0.0/batutils/tpu/size/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/size/_total.py +52 -0
- batutils-2.0.0/batutils/tpu/strimp/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/strimp/_getters.py +285 -0
- batutils-2.0.0/batutils/tpu/string/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/string/_string.py +160 -0
- batutils-2.0.0/batutils/tpu/structs/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/_base.py +3 -0
- batutils-2.0.0/batutils/tpu/structs/empty/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/empty/_empty.py +387 -0
- batutils-2.0.0/batutils/tpu/structs/enumex/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/enumex/_enumextra.py +25 -0
- batutils-2.0.0/batutils/tpu/structs/fluid/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/fluid/_attrfluid.py +77 -0
- batutils-2.0.0/batutils/tpu/structs/sentience/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/sentience/_builder.py +87 -0
- batutils-2.0.0/batutils/tpu/structs/serializers/__init__.py +3 -0
- batutils-2.0.0/batutils/tpu/structs/serializers/_json.py +12 -0
- batutils-2.0.0/batutils/tpu/structs/serializers/_jsonsafe.py +69 -0
- batutils-2.0.0/batutils/tpu/structs/serializers/_universal.py +318 -0
- batutils-2.0.0/batutils/tpu/structs/singleton/__init__.py +1 -0
- batutils-2.0.0/batutils/tpu/structs/singleton/_singleton.py +35 -0
- batutils-2.0.0/batutils.egg-info/PKG-INFO +125 -0
- batutils-2.0.0/batutils.egg-info/SOURCES.txt +76 -0
- batutils-2.0.0/batutils.egg-info/dependency_links.txt +1 -0
- batutils-2.0.0/batutils.egg-info/top_level.txt +1 -0
- batutils-2.0.0/pyproject.toml +49 -0
- batutils-2.0.0/setup.cfg +4 -0
- batutils-2.0.0/setup.py +6 -0
batutils-2.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Thordur Matthiasson
|
|
4
|
+
Copyright (c) 2013-2024 CCP Games
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
batutils-2.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: batutils
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Team Batcave's utilities for date and/or time objects and values and messing with types and so much more.
|
|
5
|
+
Author-email: Thordur Matthiasson <doddi79@gmail.com>, John Aldis <johnaldis@ccpgames.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Thordur Matthiasson
|
|
9
|
+
Copyright (c) 2013-2024 CCP Games
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: Homepage, https://github.com/doddi79/batutils
|
|
30
|
+
Project-URL: Documentation, https://github.com/doddi79/batutils/blob/main/README.md
|
|
31
|
+
Project-URL: Repository, https://github.com/doddi79/batutils.git
|
|
32
|
+
Project-URL: Issues, https://github.com/doddi79/batutils/issues
|
|
33
|
+
Project-URL: Changelog, https://github.com/doddi79/batutils/blob/main/CHANGELOG.md
|
|
34
|
+
Keywords: datetimeutils,datetime,typeutils,type,singleton,date,time,timespan,tools,batcave,utils
|
|
35
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Intended Audience :: Developers
|
|
38
|
+
Classifier: Programming Language :: Python
|
|
39
|
+
Classifier: Programming Language :: Python :: 3
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
45
|
+
Classifier: Topic :: Software Development
|
|
46
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
47
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
48
|
+
Classifier: Topic :: Utilities
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
License-File: LICENSE
|
|
51
|
+
|
|
52
|
+
# Team Batcave Python Toolkit
|
|
53
|
+
|
|
54
|
+
The `batutils` is a fork of the `batutils` package in order to continue its
|
|
55
|
+
development and maintenance beyond Team Batcave's lifespan.
|
|
56
|
+
|
|
57
|
+
It is a smooshup of a few Python packages from the CCP Tools Team of old (Team
|
|
58
|
+
Batcave).
|
|
59
|
+
|
|
60
|
+
Our two most commonly used internal packages, called `datetimeutils` and
|
|
61
|
+
`typeutils` were too _generically_ named for Pypi.org and since they were
|
|
62
|
+
both used in something like 80-90% of our other projects, it made sense to
|
|
63
|
+
just smoosh them together in one module, and thus, the `batutils` package
|
|
64
|
+
was born.
|
|
65
|
+
|
|
66
|
+
Here's the README of the [Date Time Utils](batutils/dtu/README.md) submodule.
|
|
67
|
+
|
|
68
|
+
Here's the README of the [Type Utils](batutils/tpu/README.md) submodule.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
### Changes from `ccptools`
|
|
72
|
+
|
|
73
|
+
The `legacyapi` submodule has been removed.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Date-Time Utils
|
|
77
|
+
|
|
78
|
+
The old `datetimeutils` package is now included here as the `dtu` submodule.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from batutils import dtu
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Structs
|
|
85
|
+
|
|
86
|
+
Importing `*` from the `structs` submodule will import all of the most
|
|
87
|
+
commonly used imports in our projects:
|
|
88
|
+
```python
|
|
89
|
+
from typing import * # For type annotation
|
|
90
|
+
|
|
91
|
+
import abc # For interfaces (Abstract Base Classes)
|
|
92
|
+
import dataclasses # For dataclass structs
|
|
93
|
+
import decimal # Used whenever we're handling money
|
|
94
|
+
import enum # Also used for struct creation
|
|
95
|
+
import logging # Used pretty much everywhere
|
|
96
|
+
import re # Used surprisingly frequently
|
|
97
|
+
import time # Very commonly used
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Note that datetime is not included in this. That's because tt'll also import
|
|
101
|
+
the aliases from the Datetime Utils (`batutils.dtu.structs.aliases`)
|
|
102
|
+
package instead:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
Date = datetime.date
|
|
106
|
+
Time = datetime.time
|
|
107
|
+
Datetime = datetime.datetime
|
|
108
|
+
TimeDelta = datetime.timedelta
|
|
109
|
+
TzInfo = datetime.tzinfo
|
|
110
|
+
TimeZone = datetime.timezone
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Furthermore, it'll also include a few of the most commonly used utility
|
|
114
|
+
classes from the Type Utils submodule:
|
|
115
|
+
|
|
116
|
+
- The `Singleton` Meta Class
|
|
117
|
+
- The `Empty` and `EmptyDict` classes as well as the `if_empty` method
|
|
118
|
+
- The `EnumEx` base class for Enums with the `from_any` class method
|
|
119
|
+
|
|
120
|
+
So in most cases we can cover something like 90% of any imports we tend to
|
|
121
|
+
need in every Python file with a single line:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from batutils.structs import *
|
|
125
|
+
```
|
batutils-2.0.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Team Batcave Python Toolkit
|
|
2
|
+
|
|
3
|
+
The `batutils` is a fork of the `batutils` package in order to continue its
|
|
4
|
+
development and maintenance beyond Team Batcave's lifespan.
|
|
5
|
+
|
|
6
|
+
It is a smooshup of a few Python packages from the CCP Tools Team of old (Team
|
|
7
|
+
Batcave).
|
|
8
|
+
|
|
9
|
+
Our two most commonly used internal packages, called `datetimeutils` and
|
|
10
|
+
`typeutils` were too _generically_ named for Pypi.org and since they were
|
|
11
|
+
both used in something like 80-90% of our other projects, it made sense to
|
|
12
|
+
just smoosh them together in one module, and thus, the `batutils` package
|
|
13
|
+
was born.
|
|
14
|
+
|
|
15
|
+
Here's the README of the [Date Time Utils](batutils/dtu/README.md) submodule.
|
|
16
|
+
|
|
17
|
+
Here's the README of the [Type Utils](batutils/tpu/README.md) submodule.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Changes from `ccptools`
|
|
21
|
+
|
|
22
|
+
The `legacyapi` submodule has been removed.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Date-Time Utils
|
|
26
|
+
|
|
27
|
+
The old `datetimeutils` package is now included here as the `dtu` submodule.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from batutils import dtu
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Structs
|
|
34
|
+
|
|
35
|
+
Importing `*` from the `structs` submodule will import all of the most
|
|
36
|
+
commonly used imports in our projects:
|
|
37
|
+
```python
|
|
38
|
+
from typing import * # For type annotation
|
|
39
|
+
|
|
40
|
+
import abc # For interfaces (Abstract Base Classes)
|
|
41
|
+
import dataclasses # For dataclass structs
|
|
42
|
+
import decimal # Used whenever we're handling money
|
|
43
|
+
import enum # Also used for struct creation
|
|
44
|
+
import logging # Used pretty much everywhere
|
|
45
|
+
import re # Used surprisingly frequently
|
|
46
|
+
import time # Very commonly used
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Note that datetime is not included in this. That's because tt'll also import
|
|
50
|
+
the aliases from the Datetime Utils (`batutils.dtu.structs.aliases`)
|
|
51
|
+
package instead:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
Date = datetime.date
|
|
55
|
+
Time = datetime.time
|
|
56
|
+
Datetime = datetime.datetime
|
|
57
|
+
TimeDelta = datetime.timedelta
|
|
58
|
+
TzInfo = datetime.tzinfo
|
|
59
|
+
TimeZone = datetime.timezone
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Furthermore, it'll also include a few of the most commonly used utility
|
|
63
|
+
classes from the Type Utils submodule:
|
|
64
|
+
|
|
65
|
+
- The `Singleton` Meta Class
|
|
66
|
+
- The `Empty` and `EmptyDict` classes as well as the `if_empty` method
|
|
67
|
+
- The `EnumEx` base class for Enums with the `from_any` class method
|
|
68
|
+
|
|
69
|
+
So in most cases we can cover something like 90% of any imports we tend to
|
|
70
|
+
need in every Python file with a single line:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from batutils.structs import *
|
|
74
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'decode_bytes',
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
_ENCODINGS_TO_TRY = (
|
|
6
|
+
'utf-8',
|
|
7
|
+
'latin-1',
|
|
8
|
+
'utf-16',
|
|
9
|
+
'utf-32',
|
|
10
|
+
'windows-1252',
|
|
11
|
+
'big5',
|
|
12
|
+
'shift_jis',
|
|
13
|
+
'gb18030',
|
|
14
|
+
'iso-8859-15'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decode_bytes(byte_str: bytes) -> str:
|
|
19
|
+
"""Tries to decode a bytes string into a str by trial-and-error, first using
|
|
20
|
+
the default encoding of the current runtime environment, and then by going
|
|
21
|
+
through a list of the most commonly used encodings used at the time of this
|
|
22
|
+
writing, in September 2023, as estimated by ChatGTP 4. :D
|
|
23
|
+
|
|
24
|
+
:raises ValueError: If it fails to decode the given bytes using any of the
|
|
25
|
+
encodings attempted
|
|
26
|
+
"""
|
|
27
|
+
try: # First we try to Decode with default encoding!
|
|
28
|
+
return byte_str.decode()
|
|
29
|
+
except UnicodeDecodeError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# If not, then we go through the list...!
|
|
33
|
+
for enc in _ENCODINGS_TO_TRY:
|
|
34
|
+
try:
|
|
35
|
+
return byte_str.decode(enc)
|
|
36
|
+
except UnicodeDecodeError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# If we reach here, then we've failed to decode :(
|
|
40
|
+
raise ValueError(f'failed to decode bytes to str by trial and error: {byte_str!r}')
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""This is the main API module of datetimeutils of batutils.
|
|
2
|
+
|
|
3
|
+
The intended use case is to go:
|
|
4
|
+
```python
|
|
5
|
+
from batutils import dtu
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
And then use `dtu` in your code as it serves as the "official" API of the
|
|
9
|
+
package.
|
|
10
|
+
"""
|
|
11
|
+
from batutils.dtu.structs import *
|
|
12
|
+
|
|
13
|
+
from batutils.dtu.shortcuts import *
|
|
14
|
+
|
|
15
|
+
from batutils.dtu.casting import *
|
|
16
|
+
from batutils.dtu.formatting import *
|
|
17
|
+
from batutils.dtu.parsers import *
|
|
18
|
+
|
|
19
|
+
from batutils import __version__ as VERSION # noqa
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'any_to_datetime',
|
|
3
|
+
]
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from batutils.dtu.structs import *
|
|
7
|
+
from batutils._common import *
|
|
8
|
+
from batutils.dtu.casting._filetime import *
|
|
9
|
+
from batutils.dtu.casting._string import *
|
|
10
|
+
from batutils.dtu.casting._timestamp import *
|
|
11
|
+
from batutils.dtu.casting._instant import *
|
|
12
|
+
|
|
13
|
+
_NOT_SUPPLIED = object()
|
|
14
|
+
|
|
15
|
+
_now = Datetime.now()
|
|
16
|
+
_1000_years_plus = _now.replace(_now.year + 1000)
|
|
17
|
+
_1000_years_minus = _now.replace(_now.year - 1000)
|
|
18
|
+
|
|
19
|
+
_TIMESTAMP_MIN_RANGE = datetime_to_timestamp(_1000_years_minus)
|
|
20
|
+
_TIMESTAMP_MAX_RANGE = datetime_to_timestamp(_1000_years_plus)
|
|
21
|
+
|
|
22
|
+
_INSTANT_MIN_RANGE = datetime_to_instant(_1000_years_minus)
|
|
23
|
+
_INSTANT_MAX_RANGE = datetime_to_instant(_1000_years_plus)
|
|
24
|
+
|
|
25
|
+
_REVERSE_DATETIME_REXEX = re.compile(r'(?P<day>3[01]|[012]?\d)[- /.,\\](?P<month>1[012]|0?\d)[- /.,\\]'
|
|
26
|
+
r'(?P<year>[12][0189]\d{2})(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
|
|
27
|
+
r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
|
|
28
|
+
r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
|
|
29
|
+
_REVERSE_US_DATETIME_REXEX = re.compile(r'(?P<month>1[012]|0?\d)[- /.,\\](?P<day>3[01]|[012]?\d)[- /.,\\]'
|
|
30
|
+
r'(?P<year>[12][0189]\d{2})(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
|
|
31
|
+
r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
|
|
32
|
+
r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def any_to_datetime(temporal_object: T_TEMPORAL_VALUE,
|
|
36
|
+
default: Any = _NOT_SUPPLIED) -> Union[Datetime, Any]:
|
|
37
|
+
"""Turns datetime, date, Windows filetime and posix time into a python
|
|
38
|
+
datetime if possible. By default, returns the same input value on failed
|
|
39
|
+
casting but another default return value can be given.
|
|
40
|
+
|
|
41
|
+
This function is timezone naive.
|
|
42
|
+
|
|
43
|
+
If given a number the following trickery is performed:
|
|
44
|
+
|
|
45
|
+
- If the number, treated as a timestamp, represents a datetime value that is
|
|
46
|
+
within 1000 years of now (past or future) it will be treated as a timestamp
|
|
47
|
+
because timestamps are the most commonly used numerical representations of
|
|
48
|
+
datetimes
|
|
49
|
+
- If the number is outside that range, we'll check if the number would be
|
|
50
|
+
within 1000 years of now if treated as an instant.
|
|
51
|
+
- Otherwise, we assume that such a large number must be a filetime
|
|
52
|
+
|
|
53
|
+
This does mean that there are certain cases that will yield incorrect
|
|
54
|
+
results, including:
|
|
55
|
+
|
|
56
|
+
- Timestamps more than 1000 years in the past or future (they'll be
|
|
57
|
+
treated as instants or filetimes)
|
|
58
|
+
- Instants within a couple of years of 1970 (1969-01-20 to 1971-01-22 at
|
|
59
|
+
the time of this writing) will be treated as timestamps
|
|
60
|
+
- Filetimes for the years 1600-1601 might get treated as instants or
|
|
61
|
+
timestamps
|
|
62
|
+
|
|
63
|
+
Concerning strings, there are also potential pitfalls if casting US
|
|
64
|
+
formatted YYYY-DD-MM strings, as this method will FIRST assume a standard
|
|
65
|
+
ISO format and only try the US one if that fails, so US formatted strings
|
|
66
|
+
with days between 1 and 12 will be treated as ISO and have their day and
|
|
67
|
+
month numbers switched.
|
|
68
|
+
|
|
69
|
+
Note: This is a "best-guess" method and if these edge cases are
|
|
70
|
+
unacceptable, you should totally not be using it, and instead, know exactly
|
|
71
|
+
what format your data is in and use the appropriate specific casting method.
|
|
72
|
+
"""
|
|
73
|
+
if default == _NOT_SUPPLIED:
|
|
74
|
+
default = temporal_object
|
|
75
|
+
try:
|
|
76
|
+
if isinstance(temporal_object, Datetime):
|
|
77
|
+
return temporal_object
|
|
78
|
+
|
|
79
|
+
if isinstance(temporal_object, Date):
|
|
80
|
+
return datetime.datetime.combine(temporal_object, Time())
|
|
81
|
+
|
|
82
|
+
if isinstance(temporal_object, (float, int)):
|
|
83
|
+
if _TIMESTAMP_MIN_RANGE < temporal_object < _TIMESTAMP_MAX_RANGE:
|
|
84
|
+
# This range means that the number, if treated as a timestamp,
|
|
85
|
+
# represents a datetime within 1000 years to/from now so it's the
|
|
86
|
+
# most likely bet!
|
|
87
|
+
return timestamp_to_datetime(temporal_object)
|
|
88
|
+
|
|
89
|
+
if _INSTANT_MIN_RANGE < temporal_object < _INSTANT_MAX_RANGE:
|
|
90
|
+
# This range means that the number, if treated as an instant,
|
|
91
|
+
# represents a datetime within 1000 years to/from now so it's the
|
|
92
|
+
# second most likely bet!
|
|
93
|
+
return timestamp_to_datetime(temporal_object)
|
|
94
|
+
|
|
95
|
+
# This number is so large that it's most likely a filetime!
|
|
96
|
+
return filetime_to_datetime(temporal_object)
|
|
97
|
+
|
|
98
|
+
if isinstance(temporal_object, bytes):
|
|
99
|
+
try:
|
|
100
|
+
temporal_object = decode_bytes(temporal_object)
|
|
101
|
+
except ValueError:
|
|
102
|
+
return temporal_object
|
|
103
|
+
|
|
104
|
+
if isinstance(temporal_object, str):
|
|
105
|
+
# Is this a number in string format?
|
|
106
|
+
try:
|
|
107
|
+
# Let's just try and pass this through the int caster, if that works, we evaluate it again as an int
|
|
108
|
+
return any_to_datetime(int(temporal_object))
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Let's just try and pass this through the float caster, if that works, we evaluate it again as an int
|
|
114
|
+
return any_to_datetime(float(temporal_object))
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# First we'll try the day-month-year pattern
|
|
119
|
+
value = regex_to_datetime(temporal_object, _REVERSE_DATETIME_REXEX)
|
|
120
|
+
if value:
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
# Then the month-day-year pattern
|
|
124
|
+
value = regex_to_datetime(temporal_object, _REVERSE_US_DATETIME_REXEX)
|
|
125
|
+
if value:
|
|
126
|
+
return value
|
|
127
|
+
|
|
128
|
+
# How'bout good old ISO year-month-day then? :D
|
|
129
|
+
value = isostr_to_datetime(temporal_object)
|
|
130
|
+
if value:
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
except (OverflowError, ValueError):
|
|
134
|
+
pass # Oh well
|
|
135
|
+
|
|
136
|
+
return default
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'filetime_to_datetime',
|
|
3
|
+
'datetime_to_filetime',
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
from batutils.dtu.structs import *
|
|
7
|
+
|
|
8
|
+
_FILETIME_NULL_DATE = Datetime(1601, 1, 1, 0, 0, 0)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def filetime_to_datetime(filetime: T_NUMBER) -> Datetime:
|
|
12
|
+
"""Converts a Windows file time value (number of 100-nanosecond ticks since
|
|
13
|
+
1 January 1601 00:00:00 UTC) to a standard python datetime.
|
|
14
|
+
Valid values are approx. -5.04911232e17 to 2.65046774399999999e18
|
|
15
|
+
|
|
16
|
+
This function is timezone naive.
|
|
17
|
+
|
|
18
|
+
:raise OverflowError: if filetime value is out of the range of python
|
|
19
|
+
datetime (between the year 1 and 9999 AD)
|
|
20
|
+
"""
|
|
21
|
+
return _FILETIME_NULL_DATE + TimeDelta(microseconds=(filetime / 10))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def datetime_to_filetime(dt: T_DATE_VALUE) -> int:
|
|
25
|
+
"""Converts a python datetime (or date) object to a Windows filetime value
|
|
26
|
+
(number of 100-nanosecond ticks since 1 January 1601 00:00:00 UTC).
|
|
27
|
+
|
|
28
|
+
If the supplied parameter is a date as opposed to a datetime, it will first
|
|
29
|
+
be converted locally to a datetime with the time 00:00:00.000000
|
|
30
|
+
|
|
31
|
+
This function is timezone naive.
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(dt, Datetime) and isinstance(dt, Date):
|
|
34
|
+
dt = Datetime.combine(dt, Time(0, 0, 0))
|
|
35
|
+
delta = dt - _FILETIME_NULL_DATE
|
|
36
|
+
return 10 * ((delta.days * 86400 + delta.seconds) * 1000000 + delta.microseconds)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'instant_to_datetime',
|
|
3
|
+
'datetime_to_instant',
|
|
4
|
+
]
|
|
5
|
+
from batutils.dtu.structs import *
|
|
6
|
+
from ._timestamp import *
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def instant_to_datetime(milliseconds_since_epoch: T_NUMBER, minmax_on_fail: bool = False) -> Datetime:
|
|
10
|
+
"""Converts an integer representing milliseconds since the Unix epoch
|
|
11
|
+
(January 1, 1970) to a Python datetime object.
|
|
12
|
+
|
|
13
|
+
:param milliseconds_since_epoch: Milliseconds since Unix epoch (January 1, 1970).
|
|
14
|
+
:param minmax_on_fail: If True, will return the minimum or maximum possible
|
|
15
|
+
value of Datetime in case of overflow (positive or
|
|
16
|
+
negative)
|
|
17
|
+
:return: A Python Datetime
|
|
18
|
+
"""
|
|
19
|
+
return timestamp_to_datetime(milliseconds_since_epoch / 1000., minmax_on_fail)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def datetime_to_instant(dt: T_DATE_VALUE) -> int:
|
|
23
|
+
"""Converts a Python datetime object to the number of milliseconds since
|
|
24
|
+
Unix epoch (January 1, 1970).
|
|
25
|
+
|
|
26
|
+
If given a date only, it will assume a time of 00:00:00.000000.
|
|
27
|
+
|
|
28
|
+
:param dt: Python datetime (or date).
|
|
29
|
+
:return: Number of milliseconds since Unix epoch (January 1, 1970)
|
|
30
|
+
"""
|
|
31
|
+
return int(datetime_to_timestamp(dt) * 1000)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'regex_to_datetime',
|
|
3
|
+
'isostr_to_datetime',
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
from batutils.dtu.structs import *
|
|
7
|
+
from typing import *
|
|
8
|
+
|
|
9
|
+
_T_PATTERN = Union[re.Pattern, str]
|
|
10
|
+
|
|
11
|
+
# Rexex from HELL! ;)
|
|
12
|
+
_ISODATE_REGEX = re.compile(r'^(?:(?P<year>[0]{0,3}[1-9]\d{0,3})[- /.,\\]'
|
|
13
|
+
r'(?P<month>1[012]|0?\d)[- /.,\\](?P<day>3[01]|[012]?\d))?'
|
|
14
|
+
r'(?:[ @Tt]{0,1}(?:(?P<hour>[2][0-3]|[01]?\d)[ .:,]'
|
|
15
|
+
r'(?P<minute>[012345]?\d))?(?:[ .:,](?P<second>[012345]?\d)'
|
|
16
|
+
r'(?:[ .:,](?P<millisecond>\d{0,6}))?)?)?')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def regex_to_datetime(string: str, regex_pattern: _T_PATTERN) -> Optional[Datetime]:
|
|
20
|
+
"""Given a string and a regex pattern with named groups with the same
|
|
21
|
+
keywords as the datetime object takes, this method uses that pattern to grab
|
|
22
|
+
those keywords and initialize and return a datetime object.
|
|
23
|
+
|
|
24
|
+
If only date keywords are found, the resulting datetime will have its time
|
|
25
|
+
set to midnight (0:00:00.0).
|
|
26
|
+
|
|
27
|
+
If only time keywords are found, the resulting datetime will have its date
|
|
28
|
+
set to today's date.
|
|
29
|
+
|
|
30
|
+
Failing to find a valid pattern this will simply return a None.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(regex_pattern, str):
|
|
33
|
+
regex_pattern = re.compile(regex_pattern)
|
|
34
|
+
match = regex_pattern.match(string.strip())
|
|
35
|
+
if match:
|
|
36
|
+
parts = match.groupdict()
|
|
37
|
+
found_date = False
|
|
38
|
+
if parts['year'] and parts['month'] and parts['day']:
|
|
39
|
+
date_part = Date(int(parts['year']), int(parts['month']), int(parts['day']))
|
|
40
|
+
found_date = True
|
|
41
|
+
else:
|
|
42
|
+
date_part = Date.today()
|
|
43
|
+
|
|
44
|
+
found_time = False
|
|
45
|
+
if parts['hour'] and parts['minute']:
|
|
46
|
+
time_part = Time(int(parts['hour']), int(parts['minute']), int(parts['second'] or 0), int(parts['millisecond'] or 0))
|
|
47
|
+
found_time = True
|
|
48
|
+
else:
|
|
49
|
+
time_part = Time()
|
|
50
|
+
|
|
51
|
+
if found_date or found_time:
|
|
52
|
+
return Datetime.combine(date_part, time_part)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def isostr_to_datetime(string: str) -> Optional[Datetime]:
|
|
57
|
+
"""Converts an iso(-ish) formatted string to datetime.
|
|
58
|
+
|
|
59
|
+
The pattern is quite forgiving in a few ways so context based
|
|
60
|
+
sanity-checking might be in order when parsing strings from "iffy"
|
|
61
|
+
sources (user input):
|
|
62
|
+
|
|
63
|
+
- The time part is optional, as well as individual time parts
|
|
64
|
+
- Seperator character for date parts can be: dash (-), slash (/),
|
|
65
|
+
backslash (\\), dot (.), comma (,), or space ( )
|
|
66
|
+
- Seperator character for time parts can be: colum (:), dot (.),
|
|
67
|
+
comma (,), or space ( )
|
|
68
|
+
- Seperator character of date and time halves can be: either case of
|
|
69
|
+
the letter T (T or t), the at symbol (@) or space ( )
|
|
70
|
+
- Values less than ten do not have to be zero-filled
|
|
71
|
+
("2013-1-2T3:4:5.6789" == "2013-01-02T03:04:05.6789")
|
|
72
|
+
- The year can be any value from 1-9999
|
|
73
|
+
|
|
74
|
+
Otherwise, normal datetime restrictions apply (month must be 1-12, minutes
|
|
75
|
+
hust me 0-59 etc.)
|
|
76
|
+
|
|
77
|
+
This function is timezone naive.
|
|
78
|
+
|
|
79
|
+
Failing to find a valid pattern this will simply return a None.
|
|
80
|
+
"""
|
|
81
|
+
return regex_to_datetime(string, _ISODATE_REGEX)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'timestamp_to_datetime',
|
|
3
|
+
'datetime_to_timestamp',
|
|
4
|
+
]
|
|
5
|
+
from batutils.dtu.structs import *
|
|
6
|
+
import calendar
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def timestamp_to_datetime(seconds_since_epoch: T_NUMBER, minmax_on_fail: bool = False) -> Datetime:
|
|
10
|
+
"""Converts an int or float representing seconds since the Unix epoch
|
|
11
|
+
(January 1, 1970) to a Python datetime object.
|
|
12
|
+
|
|
13
|
+
:param seconds_since_epoch: Seconds since Unix epoch (January 1, 1970).
|
|
14
|
+
:param minmax_on_fail: If True, will return the minimum or maximum possible
|
|
15
|
+
value of Datetime in case of overflow (positive or
|
|
16
|
+
negative)
|
|
17
|
+
:return: A Python Datetime
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
return Datetime.fromtimestamp(seconds_since_epoch)
|
|
21
|
+
except OSError:
|
|
22
|
+
try:
|
|
23
|
+
return Datetime(1970, 1, 1, 0, 0, 0, 0) + TimeDelta(seconds=seconds_since_epoch)
|
|
24
|
+
except OverflowError:
|
|
25
|
+
if minmax_on_fail:
|
|
26
|
+
if seconds_since_epoch > 0:
|
|
27
|
+
return Datetime.max
|
|
28
|
+
else:
|
|
29
|
+
return Datetime.min
|
|
30
|
+
else:
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def datetime_to_timestamp(dt: T_DATE_VALUE) -> float:
|
|
35
|
+
"""Converts a Python datetime object to the number of seconds since
|
|
36
|
+
Unix epoch (January 1, 1970) as a float, including fractional seconds.
|
|
37
|
+
|
|
38
|
+
If given a date only, it will assume a time of 00:00:00.000000.
|
|
39
|
+
|
|
40
|
+
:param dt: Python datetime (or date).
|
|
41
|
+
:return: Number of seconds since Unix epoch (January 1, 1970)
|
|
42
|
+
"""
|
|
43
|
+
if not isinstance(dt, Datetime) and isinstance(dt, Date):
|
|
44
|
+
dt = Datetime.combine(dt, Time(0, 0, 0, 0))
|
|
45
|
+
# TODO(thordurm@ccpgames.com>) 2024-05-22: HANDLE SECONDS!!!
|
|
46
|
+
|
|
47
|
+
int_part = float(calendar.timegm(dt.utctimetuple()))
|
|
48
|
+
if dt.microsecond:
|
|
49
|
+
int_part += dt.microsecond / 1000000.0
|
|
50
|
+
return int_part
|