dstamp 2__tar.gz → 4__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.
- dstamp-4/PKG-INFO +96 -0
- dstamp-4/README.md +75 -0
- {dstamp-2 → dstamp-4}/pyproject.toml +11 -9
- dstamp-4/src/dstamp/__init__.py +3 -0
- dstamp-4/src/dstamp/cli.py +172 -0
- dstamp-4/src/dstamp/config.py +40 -0
- dstamp-4/src/dstamp/discord.py +14 -0
- dstamp-4/src/dstamp/exceptions.py +62 -0
- dstamp-4/src/dstamp/logging.py +52 -0
- dstamp-4/src/dstamp/parse.py +129 -0
- dstamp-4/src/dstamp/round.py +88 -0
- dstamp-4/src/dstamp/subcommands/__init__.py +15 -0
- dstamp-4/src/dstamp/subcommands/get.py +182 -0
- dstamp-4/src/dstamp/subcommands/show_config.py +25 -0
- dstamp-4/src/dstamp/subcommands/show_log.py +21 -0
- dstamp-2/PKG-INFO +0 -103
- dstamp-2/README.md +0 -82
- dstamp-2/src/dstamp/__init__.py +0 -0
- dstamp-2/src/dstamp/__main__.py +0 -5
- dstamp-2/src/dstamp/config.py +0 -12
- dstamp-2/src/dstamp/console.py +0 -19
- dstamp-2/src/dstamp/format.py +0 -24
- dstamp-2/src/dstamp/main.py +0 -172
- dstamp-2/src/dstamp/parse/__init__.py +0 -15
- dstamp-2/src/dstamp/parse/datetime.py +0 -127
- dstamp-2/src/dstamp/parse/exceptions.py +0 -13
- dstamp-2/src/dstamp/parse/offset.py +0 -62
- dstamp-2/src/dstamp/parse/precision.py +0 -30
- dstamp-2/src/dstamp/round/__init__.py +0 -10
- dstamp-2/src/dstamp/round/precision.py +0 -38
- dstamp-2/src/dstamp/round/round.py +0 -30
dstamp-4/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dstamp
|
|
3
|
+
Version: 4
|
|
4
|
+
Summary: CLI app for generating timestamps for use in Discord chats.
|
|
5
|
+
Keywords: discord,generator,timestamp
|
|
6
|
+
Author: Mariusz Tang
|
|
7
|
+
Author-email: Mariusz Tang <dev@mariusztang.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Dist: argcomplete>=3.6.3
|
|
14
|
+
Requires-Dist: logassert>=8.6
|
|
15
|
+
Requires-Dist: platformdirs>=4.10.0
|
|
16
|
+
Requires-Dist: pyperclip>=1.11.0
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Project-URL: Issues, https://github.com/mariusz-tang/dstamp/issues
|
|
19
|
+
Project-URL: Repository, https://github.com/mariusz-tang/dstamp
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# dstamp
|
|
23
|
+
|
|
24
|
+
CLI app for generating timestamps for use in Discord chats.
|
|
25
|
+
|
|
26
|
+
*Note*: This is a complete rewrite of the original project from the ground up,
|
|
27
|
+
using the standard argparse module instead of cyclopts.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install using [pipx] or [uv]:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install dstamp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install dstamp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
[pipx]: https://pipx.pypa.io/stable/
|
|
42
|
+
[uv]: https://docs.astral.sh/uv/
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Get the current time.
|
|
48
|
+
dstamp get
|
|
49
|
+
|
|
50
|
+
# Get the current time, to the nearest 15 minutes.
|
|
51
|
+
dstamp get --precision 15m
|
|
52
|
+
|
|
53
|
+
# Get the time 20 minutes from now.
|
|
54
|
+
dstamp in 20m
|
|
55
|
+
|
|
56
|
+
# Show the default config file location.
|
|
57
|
+
dstamp show-config
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
See the help messages for full usage information:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
dstamp --help
|
|
64
|
+
dstamp -h get
|
|
65
|
+
dstamp -h in
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Configuration
|
|
69
|
+
|
|
70
|
+
Dstamp supports configuration by TOML file. See `dstamp -h show-config` for full
|
|
71
|
+
information. The config file location can be overridden using the `--config`
|
|
72
|
+
option.
|
|
73
|
+
|
|
74
|
+
## Logs
|
|
75
|
+
|
|
76
|
+
Dstamp writes logs to a file. Use `dstamp show-log` to get the file location.
|
|
77
|
+
|
|
78
|
+
## Shell completion
|
|
79
|
+
|
|
80
|
+
Dstamp supports shell completion using [argcomplete]. See the argcomplete
|
|
81
|
+
documentation for setup instructions.
|
|
82
|
+
|
|
83
|
+
[argcomplete]: https://github.com/kislyuk/argcomplete#activating-global-completion
|
|
84
|
+
|
|
85
|
+
## Versioning
|
|
86
|
+
|
|
87
|
+
This project uses an `MAJOR.MINOR` versioning scheme. `MAJOR` is incremented for
|
|
88
|
+
significant feature additions or changes. Everything else increments `MINOR`
|
|
89
|
+
only. `MINOR` is omitted if it is zero.
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
If you'd like to see something added to dstamp or if you would like to add something
|
|
94
|
+
yourself, please see [CONTRIBUTING](./CONTRIBUTING.md)!
|
|
95
|
+
|
|
96
|
+
Please also see the above if you have a bug to report.
|
dstamp-4/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# dstamp
|
|
2
|
+
|
|
3
|
+
CLI app for generating timestamps for use in Discord chats.
|
|
4
|
+
|
|
5
|
+
*Note*: This is a complete rewrite of the original project from the ground up,
|
|
6
|
+
using the standard argparse module instead of cyclopts.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Install using [pipx] or [uv]:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pipx install dstamp
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv tool install dstamp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
[pipx]: https://pipx.pypa.io/stable/
|
|
21
|
+
[uv]: https://docs.astral.sh/uv/
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Get the current time.
|
|
27
|
+
dstamp get
|
|
28
|
+
|
|
29
|
+
# Get the current time, to the nearest 15 minutes.
|
|
30
|
+
dstamp get --precision 15m
|
|
31
|
+
|
|
32
|
+
# Get the time 20 minutes from now.
|
|
33
|
+
dstamp in 20m
|
|
34
|
+
|
|
35
|
+
# Show the default config file location.
|
|
36
|
+
dstamp show-config
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See the help messages for full usage information:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dstamp --help
|
|
43
|
+
dstamp -h get
|
|
44
|
+
dstamp -h in
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Configuration
|
|
48
|
+
|
|
49
|
+
Dstamp supports configuration by TOML file. See `dstamp -h show-config` for full
|
|
50
|
+
information. The config file location can be overridden using the `--config`
|
|
51
|
+
option.
|
|
52
|
+
|
|
53
|
+
## Logs
|
|
54
|
+
|
|
55
|
+
Dstamp writes logs to a file. Use `dstamp show-log` to get the file location.
|
|
56
|
+
|
|
57
|
+
## Shell completion
|
|
58
|
+
|
|
59
|
+
Dstamp supports shell completion using [argcomplete]. See the argcomplete
|
|
60
|
+
documentation for setup instructions.
|
|
61
|
+
|
|
62
|
+
[argcomplete]: https://github.com/kislyuk/argcomplete#activating-global-completion
|
|
63
|
+
|
|
64
|
+
## Versioning
|
|
65
|
+
|
|
66
|
+
This project uses an `MAJOR.MINOR` versioning scheme. `MAJOR` is incremented for
|
|
67
|
+
significant feature additions or changes. Everything else increments `MINOR`
|
|
68
|
+
only. `MINOR` is omitted if it is zero.
|
|
69
|
+
|
|
70
|
+
## Contributing
|
|
71
|
+
|
|
72
|
+
If you'd like to see something added to dstamp or if you would like to add something
|
|
73
|
+
yourself, please see [CONTRIBUTING](./CONTRIBUTING.md)!
|
|
74
|
+
|
|
75
|
+
Please also see the above if you have a bug to report.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dstamp"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4"
|
|
4
4
|
description = "CLI app for generating timestamps for use in Discord chats."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -8,16 +8,16 @@ license = "MIT"
|
|
|
8
8
|
authors = [{ name = "Mariusz Tang", email = "dev@mariusztang.com" }]
|
|
9
9
|
keywords = ["discord", "generator", "timestamp"]
|
|
10
10
|
classifiers = [
|
|
11
|
-
"Development Status ::
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
12
|
"Environment :: Console",
|
|
13
13
|
"Intended Audience :: End Users/Desktop",
|
|
14
14
|
"Topic :: Communications :: Chat",
|
|
15
15
|
]
|
|
16
16
|
dependencies = [
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"argcomplete>=3.6.3",
|
|
18
|
+
"logassert>=8.6",
|
|
19
|
+
"platformdirs>=4.10.0",
|
|
19
20
|
"pyperclip>=1.11.0",
|
|
20
|
-
"pytest-mock>=3.15.1",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
@@ -25,13 +25,14 @@ Issues = "https://github.com/mariusz-tang/dstamp/issues"
|
|
|
25
25
|
Repository = "https://github.com/mariusz-tang/dstamp"
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
28
|
-
dstamp = "dstamp.
|
|
28
|
+
dstamp = "dstamp.cli:run"
|
|
29
29
|
|
|
30
30
|
[dependency-groups]
|
|
31
31
|
dev = [
|
|
32
32
|
"coverage>=7.13.5",
|
|
33
33
|
"freezegun>=1.5.5",
|
|
34
34
|
"pytest>=9.0.3",
|
|
35
|
+
"pytest-mock>=3.15.1",
|
|
35
36
|
"ty>=0.0.34",
|
|
36
37
|
]
|
|
37
38
|
|
|
@@ -43,7 +44,6 @@ build-backend = "uv_build"
|
|
|
43
44
|
branch = true
|
|
44
45
|
command_line = "-m pytest"
|
|
45
46
|
source = ["src/"]
|
|
46
|
-
omit = ["src/dstamp/__main__.py", "src/dstamp/console.py"]
|
|
47
47
|
|
|
48
48
|
[tool.coverage.report]
|
|
49
49
|
skip_empty = true
|
|
@@ -64,6 +64,7 @@ select = [
|
|
|
64
64
|
"B", # flake8-bugbear
|
|
65
65
|
"C4", # flake8-comprehensions
|
|
66
66
|
"C90", # mccabe
|
|
67
|
+
"D", # pydocstyle
|
|
67
68
|
"E", # pycodestyle error
|
|
68
69
|
"F", # pyflakes
|
|
69
70
|
"FURB", # refurb
|
|
@@ -79,5 +80,6 @@ select = [
|
|
|
79
80
|
"UP", # pyupgrade
|
|
80
81
|
"W", # pycodestyle warning
|
|
81
82
|
]
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
pydocstyle.convention = "pep257"
|
|
84
|
+
per-file-ignores = { "tests/**/*" = ["D"] }
|
|
85
|
+
flake8-annotations.allow-star-arg-any = true
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
2
|
+
"""Dstamp's CLI interface."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import logging.config
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
import tomllib
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
import argcomplete
|
|
13
|
+
import pyperclip
|
|
14
|
+
|
|
15
|
+
import dstamp.logging
|
|
16
|
+
from dstamp import config, exceptions, subcommands
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Use a base parser to change the help flag's help message.
|
|
21
|
+
base_parser = argparse.ArgumentParser(add_help=False)
|
|
22
|
+
base_parser.add_argument(
|
|
23
|
+
"-h",
|
|
24
|
+
"--help",
|
|
25
|
+
action="help",
|
|
26
|
+
help="Show this help message and exit.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def construct_parser(config: dict | None = None) -> argparse.ArgumentParser:
|
|
31
|
+
"""Create the dstamp CLI argument parser."""
|
|
32
|
+
# Add the help option manually so we can change the help message.
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
add_help=False,
|
|
35
|
+
parents=[base_parser],
|
|
36
|
+
description="Discord timestamp generator.",
|
|
37
|
+
epilog="Some options can be set via config file. See %(prog)s -h show-config",
|
|
38
|
+
suggest_on_error=True,
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--version",
|
|
42
|
+
action="version",
|
|
43
|
+
version="%(prog)s 4",
|
|
44
|
+
help="Show version number and exit.",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--config",
|
|
48
|
+
metavar="CONFIG_PATH",
|
|
49
|
+
type=pathlib.Path,
|
|
50
|
+
help="Alternative config file to use instead of the default. Should be "
|
|
51
|
+
"a TOML file.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--quiet",
|
|
55
|
+
action=argparse.BooleanOptionalAction,
|
|
56
|
+
help="Do not print warnings to the console. Disabled by default.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--verbose",
|
|
60
|
+
action=argparse.BooleanOptionalAction,
|
|
61
|
+
help="Print detailed info messages to the console. Takes precedence over "
|
|
62
|
+
"--quiet. Disabled by default.",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
subparsers = parser.add_subparsers(title="commands")
|
|
66
|
+
subcommands.register_all(subparsers, config or {})
|
|
67
|
+
|
|
68
|
+
return parser
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(args: Iterable[str] | None = None) -> None:
|
|
72
|
+
"""Parse `args` as CLI arguments and execute the resulting command."""
|
|
73
|
+
parser = construct_parser()
|
|
74
|
+
argcomplete.autocomplete(parser)
|
|
75
|
+
|
|
76
|
+
# Move help flags past subcommands so the most specific help message is shown.
|
|
77
|
+
args = _move_help_to_end(args or sys.argv[1:])
|
|
78
|
+
parsed_args = parser.parse_args(args)
|
|
79
|
+
|
|
80
|
+
# Print help if no arguments are provided.
|
|
81
|
+
if not hasattr(parsed_args, "func"):
|
|
82
|
+
parser.print_help()
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Read the config file.
|
|
86
|
+
config_path, config_, unknown_keys, config_warning = _get_config(parsed_args.config)
|
|
87
|
+
|
|
88
|
+
# Re-parse the arguments with defaults from the config.
|
|
89
|
+
# We use this method because the config path needs to be known at parse
|
|
90
|
+
# time, but we cannot determine an overridden config path before parsing.
|
|
91
|
+
parser = construct_parser(config_)
|
|
92
|
+
parsed_args = parser.parse_args(args)
|
|
93
|
+
|
|
94
|
+
# Configure logging only after reading the config.
|
|
95
|
+
verbosity = (
|
|
96
|
+
"verbose" if parsed_args.verbose else "quiet" if parsed_args.quiet else "normal"
|
|
97
|
+
)
|
|
98
|
+
logging.config.dictConfig(dstamp.logging.get_config(verbosity))
|
|
99
|
+
|
|
100
|
+
# Now we can start logging.
|
|
101
|
+
logger.info(f"args: {args or sys.argv[1:]}")
|
|
102
|
+
|
|
103
|
+
logger.info(f"using config in {config_path}")
|
|
104
|
+
if config_warning:
|
|
105
|
+
logger.warning(config_warning)
|
|
106
|
+
|
|
107
|
+
logger.info(f"computed config options: {config_}")
|
|
108
|
+
if unknown_keys:
|
|
109
|
+
logger.warning(f"unknown keys in config file: {', '.join(unknown_keys)}")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
parsed_args.func(parsed_args)
|
|
113
|
+
except exceptions.DstampError as e:
|
|
114
|
+
parser.error(str(e))
|
|
115
|
+
except pyperclip.PyperclipException as e:
|
|
116
|
+
parser.error(f"there was a problem with the clipboard manager: {e}")
|
|
117
|
+
except Exception:
|
|
118
|
+
logger.exception("An unexpected error occurred.")
|
|
119
|
+
parser.error("an unexpected error occurred; please report this to dstamp")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _move_help_to_end(args: Iterable[str]) -> Iterable[str]:
|
|
123
|
+
args = list(args)
|
|
124
|
+
if "-h" not in args and "--help" not in args:
|
|
125
|
+
return args
|
|
126
|
+
|
|
127
|
+
# Remove --version so it doesn't override the help flag if present.
|
|
128
|
+
_remove_arg(args, "--version")
|
|
129
|
+
_remove_arg(args, "--help")
|
|
130
|
+
_remove_arg(args, "-h")
|
|
131
|
+
|
|
132
|
+
args.append("--help")
|
|
133
|
+
return args
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _remove_arg(args: list[str], arg: str) -> None:
|
|
137
|
+
while arg in args:
|
|
138
|
+
args.remove(arg)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _get_config(
|
|
142
|
+
config_path_arg: pathlib.Path | None,
|
|
143
|
+
) -> tuple[pathlib.Path, dict, set, str | None]:
|
|
144
|
+
"""Get config from `config_path_arg`.
|
|
145
|
+
|
|
146
|
+
Returns the effective config path, cleaned config, unknown keys set, and
|
|
147
|
+
a warning message if applicable.
|
|
148
|
+
"""
|
|
149
|
+
log_warning = None
|
|
150
|
+
|
|
151
|
+
# Log if config path is specified at the command line but cannot be used.
|
|
152
|
+
if config_path_arg:
|
|
153
|
+
if not config_path_arg.exists():
|
|
154
|
+
log_warning = "specified config file does not exist"
|
|
155
|
+
elif not config_path_arg.is_file():
|
|
156
|
+
log_warning = "specified config path is not a file"
|
|
157
|
+
|
|
158
|
+
# Default result if we cannot read a file.
|
|
159
|
+
config_cleaned = {}
|
|
160
|
+
unknown_keys = set()
|
|
161
|
+
|
|
162
|
+
config_path = config_path_arg or config.default_path()
|
|
163
|
+
if config_path.is_file():
|
|
164
|
+
try:
|
|
165
|
+
config_raw = tomllib.loads(config_path.read_text())
|
|
166
|
+
except tomllib.TOMLDecodeError:
|
|
167
|
+
log_warning = "config file is not valid TOML"
|
|
168
|
+
else:
|
|
169
|
+
# Override result with file contents.
|
|
170
|
+
config_cleaned, unknown_keys = config.clean(config_raw)
|
|
171
|
+
|
|
172
|
+
return config_path, config_cleaned, unknown_keys, log_warning
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Config file utilities.
|
|
2
|
+
|
|
3
|
+
The functions in this module do not create a config file if one does not exist.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pathlib
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import platformdirs
|
|
10
|
+
|
|
11
|
+
import dstamp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_path() -> pathlib.Path:
|
|
15
|
+
"""Return the default config file path.
|
|
16
|
+
|
|
17
|
+
Note that the file does not necessarily exist.
|
|
18
|
+
"""
|
|
19
|
+
config_dir = platformdirs.user_config_path(dstamp.APP_NAME)
|
|
20
|
+
return config_dir / "config.toml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_VALID_KEYS = {
|
|
24
|
+
"copy",
|
|
25
|
+
"format",
|
|
26
|
+
"precision",
|
|
27
|
+
"quiet",
|
|
28
|
+
"verbose",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def clean(config: dict) -> tuple[dict[str, Any], set[str]]:
|
|
33
|
+
"""Clean a config dict so it contains only valid keys.
|
|
34
|
+
|
|
35
|
+
Returns a dictionary containing the cleaned config and a set containing the
|
|
36
|
+
invalid keys that were found in the input.
|
|
37
|
+
"""
|
|
38
|
+
cleaned_config = {key: value for key, value in config.items() if key in _VALID_KEYS}
|
|
39
|
+
unknown_keys = config.keys() - _VALID_KEYS
|
|
40
|
+
return cleaned_config, unknown_keys
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Discord-related formatting utilities."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def timestamp(time: datetime, format_code: str) -> str:
|
|
7
|
+
"""Return a Discord timestamp representing `time`.
|
|
8
|
+
|
|
9
|
+
:param time: The time to respresent. If it has a fractional timestamp, it
|
|
10
|
+
is truncated towards zero.
|
|
11
|
+
:param format_code: The format code to use. Tells Discord how to format the
|
|
12
|
+
timestamp.
|
|
13
|
+
"""
|
|
14
|
+
return f"<t:{int(time.timestamp())}:{format_code}>"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Exception classes for errors raised by dstamp."""
|
|
2
|
+
|
|
3
|
+
from dstamp import round
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DstampError(Exception):
|
|
7
|
+
"""Base class for all exceptions raised by dstamp."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParserInputError(DstampError):
|
|
11
|
+
"""Base class for parser input errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, input: str, output_type: type) -> None:
|
|
14
|
+
"""Initialize an error instance with a formatted error message.
|
|
15
|
+
|
|
16
|
+
:param input: The input string which caused the parser to fail.
|
|
17
|
+
:param output_type: The expected output type.
|
|
18
|
+
"""
|
|
19
|
+
self.input = input
|
|
20
|
+
self.output_type = output_type
|
|
21
|
+
super().__init__(self._format_message())
|
|
22
|
+
|
|
23
|
+
def _format_message(self) -> str:
|
|
24
|
+
raise NotImplementedError # pragma: no cover
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParserFormatError(ParserInputError):
|
|
28
|
+
"""Raised when the input to a parser was not in the right format."""
|
|
29
|
+
|
|
30
|
+
def _format_message(self) -> str:
|
|
31
|
+
return f"invalid {self.output_type.__name__.lower()} format: {repr(self.input)}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ParserValueError(ParserInputError):
|
|
35
|
+
"""Raised when an invalid time object would be created."""
|
|
36
|
+
|
|
37
|
+
def _format_message(self) -> str:
|
|
38
|
+
return (
|
|
39
|
+
f"input represents an invalid {self.output_type.__name__.lower()}: "
|
|
40
|
+
f"{repr(self.input)}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PrecisionQuantityError(DstampError):
|
|
45
|
+
"""Raised when the `Precision` constructor receives an invalid quantity."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, quantity: int, unit: round.Unit) -> None:
|
|
48
|
+
"""Initialize an error instance with a formatted error message.
|
|
49
|
+
|
|
50
|
+
:param quantity: The quantity passed to the `Precision` constructor.
|
|
51
|
+
:param unit: The unit object passed to the `Precision` constructor.
|
|
52
|
+
"""
|
|
53
|
+
self.quantity = quantity
|
|
54
|
+
self.unit = unit
|
|
55
|
+
super().__init__(self._format_message())
|
|
56
|
+
|
|
57
|
+
def _format_message(self) -> str:
|
|
58
|
+
return (
|
|
59
|
+
f"quantity for {self.unit.name.lower()} must be between 1 and "
|
|
60
|
+
f"{self.unit.max_quantity} (inclusive), and must be a factor of "
|
|
61
|
+
f"{self.unit.max_quantity} (actual quantity was {self.quantity})."
|
|
62
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Defines dstamp's logging configuration."""
|
|
2
|
+
|
|
3
|
+
import platformdirs
|
|
4
|
+
|
|
5
|
+
import dstamp
|
|
6
|
+
|
|
7
|
+
LOG_DIR = platformdirs.user_log_path(dstamp.APP_NAME, ensure_exists=True)
|
|
8
|
+
|
|
9
|
+
CONFIG = {
|
|
10
|
+
"version": 1,
|
|
11
|
+
"formatters": {
|
|
12
|
+
"short": {"format": "%(levelname)s: %(message)s"},
|
|
13
|
+
"full": {"format": "%(asctime)s %(levelname)s: %(name)s: %(message)s"},
|
|
14
|
+
},
|
|
15
|
+
"handlers": {
|
|
16
|
+
"console": {
|
|
17
|
+
"class": "logging.StreamHandler",
|
|
18
|
+
"formatter": "short",
|
|
19
|
+
"level": "WARNING",
|
|
20
|
+
},
|
|
21
|
+
"file": {
|
|
22
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
23
|
+
"formatter": "full",
|
|
24
|
+
"filename": LOG_DIR / f"{dstamp.APP_NAME}.log",
|
|
25
|
+
"encoding": "utf-8",
|
|
26
|
+
"maxBytes": 50_000,
|
|
27
|
+
"backupCount": 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"root": {
|
|
31
|
+
"level": "INFO",
|
|
32
|
+
"handlers": ["console", "file"],
|
|
33
|
+
},
|
|
34
|
+
"disable_existing_loggers": False,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_config(verbosity: str) -> dict:
|
|
39
|
+
"""Get an appropriate logging configuration for the given `verbosity`.
|
|
40
|
+
|
|
41
|
+
:param verbosity: recognized values are "quiet" and "verbose". Other values
|
|
42
|
+
are ignored and produce the default config.
|
|
43
|
+
"""
|
|
44
|
+
# Use unspecific type annotation to avoid type errors below.
|
|
45
|
+
base: dict = CONFIG
|
|
46
|
+
|
|
47
|
+
if verbosity == "quiet":
|
|
48
|
+
base["root"]["handlers"].remove("console")
|
|
49
|
+
elif verbosity == "verbose":
|
|
50
|
+
base["handlers"]["console"]["level"] = "INFO"
|
|
51
|
+
|
|
52
|
+
return base
|