nbdantic 0.1.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.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: nbdantic
3
+ Version: 0.1.0
4
+ Requires-Dist: ansiwrap>=0.8.4
5
+ Requires-Dist: nbformat>=5.10.4
6
+ Requires-Dist: tomlkit>=0.13.0
7
+ Summary: notebook parsing minimum DSL
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
10
+ Project-URL: Documentation, https://github.com/ivanbelenky/nbdantic#readme
11
+ Project-URL: Homepage, https://github.com/ivanbelenky/nbdantic
12
+ Project-URL: Issues, https://github.com/ivanbelenky/nbdantic/issues
13
+ Project-URL: Repository, https://github.com/ivanbelenky/nbdantic
14
+
15
+ # nbdantic ◬
16
+
17
+ fully typed context-free grammar for jupyter notebooks. declare expected structure, validate against it.
18
+
19
+ ## install
20
+
21
+ ```
22
+ uv add nbdantic
23
+ ```
24
+
25
+ ## grammar
26
+
27
+ | production | rule | note |
28
+ |---|---|---|
29
+ | Notebook | Cell\* Sequence Cell_terminal? | |
30
+ | Sequence | (Element …)+ \| ε | `empty=True` allows ε |
31
+ | Element | Markdown \| Code \| Sequence \| Choice \| Maybe | |
32
+ | Markdown | cell\<markdown\> | terminal |
33
+ | Code | cell\<code\> | terminal |
34
+ | Choice | Element \| Element \| … | first match wins |
35
+ | Maybe | Element \| ε | |
36
+
37
+ ### grammar elements
38
+
39
+ ```python
40
+
41
+ type ParseElement = Markdown | Code | Sequence | Choice | Maybe
42
+
43
+ # Markdown and Code are self explanatory
44
+ # Sequence is None one or more elements (enabling nesting of structures/sequences)
45
+ # Choice lets you pick from a set of Elements (it could be arbitrary sets of nested pattenrs)
46
+ # Maybe allows for expressing Optiona<Element>
47
+ ```
48
+
49
+
50
+ ### usage
51
+
52
+ ```python
53
+ import ast # for custom validation function
54
+
55
+ from nbdantic import Code, Markdown, Maybe, Sequence, JupyterNotebook
56
+ from nbdantic.validators import valid_python, not_empty, at_least
57
+
58
+
59
+ def only_imports(c):
60
+ tree = ast.parse(c)
61
+ if not all(
62
+ isinstance(n, (ast.Import, ast.ImportFrom, ast.alias, ast.Module)) for n in ast.walk(tree)
63
+ ):
64
+ raise ValueError("code should only contain import statements")
65
+
66
+
67
+ class Paper(JupyterNotebook):
68
+ structure = Sequence("root", [
69
+ Markdown("title", validators=[not_empty]),
70
+ Code("imports", validators=[valid_python, only_imports]),
71
+ Maybe("abstract", Markdown("abstract_md")),
72
+ Sequence("sections", [
73
+ Markdown("heading"),
74
+ Code("body", validators=[valid_python]),
75
+ ], validators=[at_least(2)], empty=True),
76
+ ])
77
+
78
+ result = Paper.from_file("paper.ipynb").validate()
79
+ result.raise_if_failed()
80
+ result.cells_map
81
+ ```
82
+
83
+
84
+ ### extra features
85
+
86
+ #### built-in validators
87
+
88
+ `nbdantic.validators` has some common checks, some of them are simple and example like, some of them may prove useful, like
89
+ - `ty_check`
90
+ - `ruff_check`
91
+ - `line_warning`
92
+
93
+
94
+ #### script runner
95
+
96
+ `nbdantic.script_runner.UVScriptRunner` spins up isolated uv environments for running code, linting, type checking, whatever you need. validators like `ruff_check` and `ty_check` use it under the hood.
97
+
98
+ ```python
99
+ from nbdantic.script_runner import UVScriptRunner
100
+
101
+ with UVScriptRunner(packages=["numpy>=1.24"], python_version="3.13") as runner:
102
+ result = runner.oneshot_python("import numpy; print(numpy.__version__)")
103
+ print(result.stdout)
104
+ ```
105
+
106
+
107
+ ### notes
108
+ pep-508/440 parsing backed by [uv-pep508 & uv-pep440 crates](https://github.com/astral-sh/uv) via pyo3. Thanks uv for everything.
109
+
@@ -0,0 +1,94 @@
1
+ # nbdantic ◬
2
+
3
+ fully typed context-free grammar for jupyter notebooks. declare expected structure, validate against it.
4
+
5
+ ## install
6
+
7
+ ```
8
+ uv add nbdantic
9
+ ```
10
+
11
+ ## grammar
12
+
13
+ | production | rule | note |
14
+ |---|---|---|
15
+ | Notebook | Cell\* Sequence Cell_terminal? | |
16
+ | Sequence | (Element …)+ \| ε | `empty=True` allows ε |
17
+ | Element | Markdown \| Code \| Sequence \| Choice \| Maybe | |
18
+ | Markdown | cell\<markdown\> | terminal |
19
+ | Code | cell\<code\> | terminal |
20
+ | Choice | Element \| Element \| … | first match wins |
21
+ | Maybe | Element \| ε | |
22
+
23
+ ### grammar elements
24
+
25
+ ```python
26
+
27
+ type ParseElement = Markdown | Code | Sequence | Choice | Maybe
28
+
29
+ # Markdown and Code are self explanatory
30
+ # Sequence is None one or more elements (enabling nesting of structures/sequences)
31
+ # Choice lets you pick from a set of Elements (it could be arbitrary sets of nested pattenrs)
32
+ # Maybe allows for expressing Optiona<Element>
33
+ ```
34
+
35
+
36
+ ### usage
37
+
38
+ ```python
39
+ import ast # for custom validation function
40
+
41
+ from nbdantic import Code, Markdown, Maybe, Sequence, JupyterNotebook
42
+ from nbdantic.validators import valid_python, not_empty, at_least
43
+
44
+
45
+ def only_imports(c):
46
+ tree = ast.parse(c)
47
+ if not all(
48
+ isinstance(n, (ast.Import, ast.ImportFrom, ast.alias, ast.Module)) for n in ast.walk(tree)
49
+ ):
50
+ raise ValueError("code should only contain import statements")
51
+
52
+
53
+ class Paper(JupyterNotebook):
54
+ structure = Sequence("root", [
55
+ Markdown("title", validators=[not_empty]),
56
+ Code("imports", validators=[valid_python, only_imports]),
57
+ Maybe("abstract", Markdown("abstract_md")),
58
+ Sequence("sections", [
59
+ Markdown("heading"),
60
+ Code("body", validators=[valid_python]),
61
+ ], validators=[at_least(2)], empty=True),
62
+ ])
63
+
64
+ result = Paper.from_file("paper.ipynb").validate()
65
+ result.raise_if_failed()
66
+ result.cells_map
67
+ ```
68
+
69
+
70
+ ### extra features
71
+
72
+ #### built-in validators
73
+
74
+ `nbdantic.validators` has some common checks, some of them are simple and example like, some of them may prove useful, like
75
+ - `ty_check`
76
+ - `ruff_check`
77
+ - `line_warning`
78
+
79
+
80
+ #### script runner
81
+
82
+ `nbdantic.script_runner.UVScriptRunner` spins up isolated uv environments for running code, linting, type checking, whatever you need. validators like `ruff_check` and `ty_check` use it under the hood.
83
+
84
+ ```python
85
+ from nbdantic.script_runner import UVScriptRunner
86
+
87
+ with UVScriptRunner(packages=["numpy>=1.24"], python_version="3.13") as runner:
88
+ result = runner.oneshot_python("import numpy; print(numpy.__version__)")
89
+ print(result.stdout)
90
+ ```
91
+
92
+
93
+ ### notes
94
+ pep-508/440 parsing backed by [uv-pep508 & uv-pep440 crates](https://github.com/astral-sh/uv) via pyo3. Thanks uv for everything.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.0,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "nbdantic"
7
+ version = "0.1.0"
8
+ description = "notebook parsing minimum DSL"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "ansiwrap>=0.8.4",
13
+ "nbformat>=5.10.4",
14
+ "tomlkit>=0.13.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/ivanbelenky/nbdantic"
19
+ Repository = "https://github.com/ivanbelenky/nbdantic"
20
+ Issues = "https://github.com/ivanbelenky/nbdantic/issues"
21
+ Documentation = "https://github.com/ivanbelenky/nbdantic#readme"
22
+
23
+ [tool.maturin]
24
+ manifest-path = "src-rs/Cargo.toml"
25
+ python-source = "src"
26
+ module-name = "nbdantic.nbrs"
27
+ features = ["pyo3/extension-module"]
28
+
29
+ [tool.ruff]
30
+ indent-width = 4
31
+ line-length = 120
32
+
33
+ [tool.ruff.lint]
34
+ select = ["E", "F", "A"]
35
+ ignore = ["E501"]
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "ipython>=8.31.0",
40
+ "coverage>=7.6.12",
41
+ "maturin>=1.0,<2.0",
42
+ "pre-commit>=4.0.1",
43
+ "ipykernel>=6.30.1",
44
+ "pyright>=1.1.407",
45
+ "ty>=0.0.1a7",
46
+ "pytest>=9.0.2",
47
+ ]
48
+
49
+
50
+ [tool.pytest.ini_options]
51
+ norecursedirs = ["scratch"]
@@ -0,0 +1,35 @@
1
+ from .grammar import (
2
+ Cell,
3
+ Choice,
4
+ Code,
5
+ Markdown,
6
+ Maybe,
7
+ Sequence,
8
+ )
9
+ from .logger import set_nocolor
10
+ from .parser import (
11
+ JupyterNotebook,
12
+ ValidationFailure,
13
+ ValidationResult,
14
+ )
15
+ from .types import (
16
+ CellNode,
17
+ CellsMap,
18
+ ChoiceResult,
19
+ )
20
+
21
+ __all__ = [
22
+ "Cell",
23
+ "CellNode",
24
+ "CellsMap",
25
+ "Choice",
26
+ "ChoiceResult",
27
+ "Code",
28
+ "JupyterNotebook",
29
+ "Markdown",
30
+ "Maybe",
31
+ "Sequence",
32
+ "ValidationFailure",
33
+ "ValidationResult",
34
+ "set_nocolor",
35
+ ]
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from copy import deepcopy
5
+ from textwrap import dedent
6
+ from typing import Any, Callable, ClassVar, Final, Iterator, cast
7
+
8
+ from .logger import cstr
9
+ from .types import CellContent, CellValidator, Label, SequenceValidator
10
+
11
+
12
+ def _validator_name(v: Callable[..., Any]) -> str:
13
+ return getattr(v, "__name__", type(v).__name__)
14
+
15
+
16
+ def _check_cell_validator(v: CellValidator) -> None:
17
+ try:
18
+ sig = inspect.signature(v)
19
+ except (ValueError, TypeError):
20
+ return
21
+ params = [
22
+ p
23
+ for p in sig.parameters.values()
24
+ if p.default is inspect.Parameter.empty
25
+ and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
26
+ ]
27
+ if len(params) != 1:
28
+ raise TypeError(
29
+ f"CellValidator '{_validator_name(v)}' must accept exactly 1 positional "
30
+ f"parameter (CellContent), got {len(params)}"
31
+ )
32
+
33
+
34
+ def _check_sequence_validator(v: SequenceValidator) -> None:
35
+ try:
36
+ sig = inspect.signature(v)
37
+ except (ValueError, TypeError):
38
+ return
39
+ params = [
40
+ p
41
+ for p in sig.parameters.values()
42
+ if p.default is inspect.Parameter.empty
43
+ and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
44
+ ]
45
+ if len(params) != 1:
46
+ raise TypeError(
47
+ f"SequenceValidator '{_validator_name(v)}' must accept exactly 1 positional "
48
+ f"parameter (SequenceContent), got {len(params)}"
49
+ )
50
+
51
+
52
+ class CellValidationError(Exception):
53
+ def __init__(self, message: str, validator_name: str | None = None) -> None:
54
+ super().__init__(message)
55
+ self.validator_name = validator_name
56
+
57
+
58
+ class SequenceError(Exception):
59
+ pass
60
+
61
+
62
+ class Cell:
63
+ cell_type: ClassVar[str]
64
+
65
+ def __init__(
66
+ self,
67
+ label: Label,
68
+ validators: list[CellValidator] | None = None,
69
+ source: CellContent = "",
70
+ ) -> None:
71
+ self.label: Final[Label] = label
72
+ self._source = source
73
+ self.validators: list[CellValidator] = []
74
+ for v in validators or []:
75
+ _check_cell_validator(v)
76
+ self.validators.append(v)
77
+
78
+ @property
79
+ def content(self) -> CellContent:
80
+ return self._source
81
+
82
+ def validate(self, x: CellContent) -> None:
83
+ for v in self.validators:
84
+ try:
85
+ v(deepcopy(x))
86
+ except Exception as e:
87
+ doc = f"({v.__doc__})" if v.__doc__ else ""
88
+ raise CellValidationError(
89
+ dedent(f"""
90
+ validation failed for cell '{cstr("yellow", self.label, True)}':\n
91
+ {cstr("magenta", _validator_name(v), True)} {doc} - {e!s}
92
+ """),
93
+ validator_name=_validator_name(v),
94
+ )
95
+
96
+ def __or__(self, other: Choice | Sequence | Cell | Maybe) -> Choice:
97
+ if isinstance(other, Choice):
98
+ return Choice(
99
+ label=f"{self.label}_or_{'_or_'.join(o.label for o in other.options)}",
100
+ options=cast(list, [self] + other.options),
101
+ )
102
+ return Choice(
103
+ label=f"{self.label}_or_{other.label}",
104
+ options=cast(list, [self, other]),
105
+ )
106
+
107
+
108
+ class Markdown(Cell):
109
+ cell_type = "markdown"
110
+
111
+
112
+ class Code(Cell):
113
+ cell_type = "code"
114
+
115
+
116
+ class Choice:
117
+ def __init__(
118
+ self,
119
+ label: Label,
120
+ options: list[Markdown | Code | Sequence | Choice | Maybe],
121
+ ) -> None:
122
+ self.label: Final[Label] = label
123
+ self.options: list[Markdown | Code | Sequence | Choice | Maybe] = []
124
+ for opt in options:
125
+ if isinstance(opt, Choice):
126
+ self.options.extend(opt.options)
127
+ else:
128
+ self.options.append(opt)
129
+
130
+ def __or__(self, other: Choice | Sequence | Cell | Maybe) -> Choice:
131
+ if isinstance(other, Choice):
132
+ return Choice(
133
+ label=f"{self.label}_or_{other.label}",
134
+ options=self.options + other.options,
135
+ )
136
+ return Choice(
137
+ label=f"{self.label}_or_{other.label}",
138
+ options=cast(list, self.options + [other]),
139
+ )
140
+
141
+
142
+ class Maybe:
143
+ def __init__(
144
+ self,
145
+ label: Label,
146
+ element: Markdown | Code | Sequence | Choice,
147
+ ) -> None:
148
+ self.label: Final[Label] = label
149
+ self.element: Final[Markdown | Code | Sequence | Choice] = element
150
+
151
+ def __or__(self, other: Choice | Sequence | Cell | Maybe) -> Choice:
152
+ if isinstance(other, Choice):
153
+ return Choice(
154
+ label=f"{self.label}_or_{'_or_'.join(o.label for o in other.options)}",
155
+ options=cast(list, [self] + other.options),
156
+ )
157
+ return Choice(
158
+ label=f"{self.label}_or_{other.label}",
159
+ options=cast(list, [self, other]),
160
+ )
161
+
162
+
163
+ class Sequence:
164
+ def __init__(
165
+ self,
166
+ label: Label,
167
+ structure: list[Markdown | Code | Sequence | Choice | Maybe],
168
+ validators: list[SequenceValidator] | None = None,
169
+ empty: bool = False,
170
+ ) -> None:
171
+ self.label: Final[Label] = label
172
+ self.empty: Final[bool] = empty
173
+ self.structure: Final[list[Markdown | Code | Sequence | Choice | Maybe]] = structure
174
+ self.validators: list[SequenceValidator] = []
175
+ for v in validators or []:
176
+ _check_sequence_validator(v)
177
+ self.validators.append(v)
178
+
179
+ def __iter__(self) -> Iterator[Markdown | Code | Sequence | Choice | Maybe]:
180
+ yield from self.structure
181
+
182
+ def __or__(self, other: Choice | Sequence | Cell | Maybe) -> Choice:
183
+ if isinstance(other, Choice):
184
+ return Choice(
185
+ label=f"{self.label}_or_{'_or_'.join(o.label for o in other.options)}",
186
+ options=cast(list, [self] + other.options),
187
+ )
188
+ return Choice(
189
+ label=f"{self.label}_or_{other.label}",
190
+ options=cast(list, [self, other]),
191
+ )
192
+
193
+
194
+ type ParseElement = Markdown | Code | Sequence | Choice | Maybe
@@ -0,0 +1,181 @@
1
+ import importlib
2
+ import importlib.util
3
+ import logging
4
+ import re
5
+ import sys
6
+ from enum import Enum
7
+
8
+ try:
9
+
10
+ def _find_module(module: str) -> list[str]:
11
+ spec = importlib.util.find_spec(module)
12
+ return [spec.name] if spec else []
13
+
14
+ def _load_module(_: str, module: str) -> object:
15
+ return importlib.import_module(module)
16
+
17
+ setattr(importlib, "find_module", _find_module)
18
+ setattr(importlib, "load_module", _load_module)
19
+ sys.modules["imp"] = importlib
20
+
21
+ from ansiwrap import wrap as _wrap
22
+ except ImportError:
23
+ from textwrap import wrap as _wrap
24
+
25
+ STATUS_LEN = 88
26
+
27
+ _ANSI: dict[str, str] = {
28
+ "red": "31",
29
+ "green": "32",
30
+ "yellow": "33",
31
+ "blue": "34",
32
+ "magenta": "35",
33
+ "cyan": "36",
34
+ "white": "37",
35
+ "black": "30",
36
+ "orange": "38;5;208",
37
+ "purple": "38;5;141",
38
+ "pink": "38;5;217",
39
+ }
40
+
41
+ _STATUS_COLORS: dict[str, str] = {
42
+ "STARTED": "yellow",
43
+ "PASSED": "green",
44
+ "FAILED": "red",
45
+ "WARNING": "orange",
46
+ "INFO": "purple",
47
+ }
48
+
49
+ _nocolor: bool = False
50
+
51
+
52
+ def set_nocolor(enabled: bool = True) -> None:
53
+ global _nocolor
54
+ _nocolor = enabled
55
+ _handler.setFormatter(PlainFormatter() if enabled else StatusFormatter())
56
+
57
+
58
+ class LogStatus(Enum):
59
+ STARTED = "STARTED"
60
+ PASSED = "PASSED"
61
+ FAILED = "FAILED"
62
+ WARNING = "WARNING"
63
+ INFO = "INFO"
64
+
65
+
66
+ # NOTE: inspired always by geohotz tinygrad/helpers.py `colored()``
67
+ def cstr(color: str | None, text: object, bold: bool = False) -> str:
68
+ if _nocolor or color is None or color not in _ANSI:
69
+ return str(text)
70
+ b = "1;" if bold else ""
71
+ return f"\033[{b}{_ANSI[color]}m{text}\033[0m"
72
+
73
+
74
+ def ansistrip(s: str) -> str:
75
+ return re.sub(r"\x1b\[(K|.*?m)", "", s)
76
+
77
+
78
+ def ansilen(s: str) -> int:
79
+ return len(ansistrip(s))
80
+
81
+
82
+ def word_wrap(s: str, width: int = 80) -> str:
83
+ if ansilen(s) <= width:
84
+ return s
85
+ lines = s.splitlines()
86
+ if len(lines) > 1:
87
+ return "\n".join(word_wrap(line, width) for line in lines)
88
+ i = 0
89
+ while ansilen(s[:i]) < width and i < len(s):
90
+ i += 1
91
+ return s[:i] + "\n" + word_wrap(s[i:], width)
92
+
93
+
94
+ class PlainFormatter(logging.Formatter):
95
+ def format(self, record: logging.LogRecord) -> str:
96
+ status: LogStatus | None = getattr(record, "status", None)
97
+ if status is None:
98
+ return super().format(record)
99
+
100
+ msg = record.getMessage()
101
+ lines = msg.split("\n")
102
+ all_chunks: list[str] = []
103
+ for line in lines:
104
+ chunks = _wrap(line, width=STATUS_LEN - 1, break_long_words=False) if line.strip() else [""]
105
+ all_chunks.extend(chunks)
106
+
107
+ parts: list[str] = []
108
+ if status == LogStatus.WARNING:
109
+ parts.append(f"{'-' * (STATUS_LEN - 1)} [{status.value}]")
110
+
111
+ first_line = f"{all_chunks[0]:<{STATUS_LEN - 1}}"
112
+ if status == LogStatus.WARNING:
113
+ parts.append(first_line)
114
+ else:
115
+ parts.append(f"{first_line} [{status.value}]")
116
+
117
+ for chunk in all_chunks[1:]:
118
+ parts.append(chunk)
119
+
120
+ if status == LogStatus.WARNING:
121
+ parts.append("-" * (STATUS_LEN - 1))
122
+
123
+ return "\n".join(parts)
124
+
125
+
126
+ class StatusFormatter(logging.Formatter):
127
+ def format(self, record: logging.LogRecord) -> str:
128
+ status: LogStatus | None = getattr(record, "status", None)
129
+ nocolor: bool = getattr(record, "nocolor", False)
130
+
131
+ if status is None:
132
+ return super().format(record)
133
+
134
+ msg = record.getMessage()
135
+ color = _STATUS_COLORS.get(status.value)
136
+ status_tag = cstr(color, f" [{status.value}]", bold=True)
137
+
138
+ lines = msg.split("\n")
139
+ all_chunks: list[str] = []
140
+ for line in lines:
141
+ chunks = _wrap(line, width=STATUS_LEN - 1, break_long_words=False) if line.strip() else [""]
142
+ all_chunks.extend(chunks)
143
+
144
+ text_color = "orange" if status == LogStatus.WARNING and not nocolor else None
145
+ parts: list[str] = []
146
+
147
+ if status == LogStatus.WARNING:
148
+ parts.append(f"{'-' * (STATUS_LEN - 1)}{status_tag}")
149
+
150
+ visible_len = ansilen(all_chunks[0])
151
+ pad = max(STATUS_LEN - 1 - visible_len, 0)
152
+ first_line = all_chunks[0] + " " * pad
153
+ if status == LogStatus.WARNING:
154
+ parts.append(cstr(text_color, first_line))
155
+ else:
156
+ parts.append(cstr(text_color, first_line) + status_tag)
157
+
158
+ for chunk in all_chunks[1:]:
159
+ parts.append(cstr(text_color, chunk))
160
+
161
+ if status == LogStatus.WARNING:
162
+ parts.append("-" * (STATUS_LEN - 1))
163
+
164
+ return "\n".join(parts)
165
+
166
+
167
+ class NbdanticLogger(logging.Logger):
168
+ def status_log(self, msg: str, status: LogStatus, nocolor: bool = False) -> None:
169
+ level = logging.WARNING if status == LogStatus.WARNING else logging.INFO
170
+ self.log(level, msg, extra={"status": status, "nocolor": nocolor})
171
+
172
+
173
+ logging.setLoggerClass(NbdanticLogger)
174
+
175
+ logger: NbdanticLogger = logging.getLogger("nbdantic") # type: ignore
176
+
177
+ _handler = logging.StreamHandler()
178
+ _handler.setFormatter(StatusFormatter())
179
+ logger.addHandler(_handler)
180
+ logger.setLevel(logging.INFO)
181
+ logger.propagate = False
@@ -0,0 +1,27 @@
1
+ from enum import IntEnum
2
+
3
+ class VersionT(IntEnum):
4
+ Url = ...
5
+ VerSpec = ...
6
+
7
+ class VersionSpecifier:
8
+ @property
9
+ def operator(self) -> str: ...
10
+ @property
11
+ def version(self) -> str: ...
12
+ def __repr__(self) -> str: ...
13
+ def __str__(self) -> str: ...
14
+
15
+ class UVRequirement:
16
+ @property
17
+ def package_name(self) -> str: ...
18
+ @property
19
+ def version_type(self) -> VersionT | None: ...
20
+ @property
21
+ def pkg_spec(self) -> list[VersionSpecifier] | None: ...
22
+ @property
23
+ def version_str(self) -> str | None: ...
24
+ def __repr__(self) -> str: ...
25
+
26
+ def version_specs_from_string(spec_str: str) -> list[VersionSpecifier]: ...
27
+ def package_spec_from_string(spec_str: str) -> UVRequirement: ...