bugdantic 0.2.8__tar.gz → 0.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.
- {bugdantic-0.2.8 → bugdantic-0.3.0}/PKG-INFO +2 -2
- bugdantic-0.3.0/bugdantic/userstory.py +130 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic.egg-info/PKG-INFO +2 -2
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic.egg-info/SOURCES.txt +3 -1
- {bugdantic-0.2.8 → bugdantic-0.3.0}/pyproject.toml +2 -2
- bugdantic-0.3.0/test/test_userstory.py +59 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/LICENSE +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/README.md +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic/__init__.py +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic/bugzilla.py +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic/py.typed +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic.egg-info/dependency_links.txt +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic.egg-info/requires.txt +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/bugdantic.egg-info/top_level.txt +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/setup.cfg +0 -0
- {bugdantic-0.2.8 → bugdantic-0.3.0}/test/test_bugzilla.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugdantic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Bugzilla client based on pydantic
|
|
5
5
|
Author-email: James Graham <james@hoppipolla.co.uk>
|
|
6
|
-
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
7
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
8
8
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
9
9
|
Requires-Python: >=3.11
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Utility functions for working with cf_user_story fields that
|
|
2
|
+
are used to store key: value pair data"""
|
|
3
|
+
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Iterator, Optional, Mapping, Self
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse(
|
|
13
|
+
user_story: str,
|
|
14
|
+
) -> Iterator[tuple[str, Optional[str], Optional[str]]]:
|
|
15
|
+
"""Parse the user story assuming it's lines of the form key: value.
|
|
16
|
+
|
|
17
|
+
If there isn't a colon in the line we simply set value to the full line."""
|
|
18
|
+
user_story_re = re.compile(r"^\s*([^\s]+)\s*:\s*(.*)")
|
|
19
|
+
for line in user_story.splitlines():
|
|
20
|
+
key = None
|
|
21
|
+
value = None
|
|
22
|
+
m = user_story_re.match(line)
|
|
23
|
+
if m is not None:
|
|
24
|
+
maybe_key, maybe_value = m.groups()
|
|
25
|
+
if maybe_value:
|
|
26
|
+
key = maybe_key
|
|
27
|
+
value = maybe_value
|
|
28
|
+
yield line, key, value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_as_dict(user_story: str) -> Mapping[str, str | list[str]]:
|
|
32
|
+
rv: dict[str, str | list[str]] = {}
|
|
33
|
+
for _, key, value in parse(user_story):
|
|
34
|
+
if key is not None and value is not None:
|
|
35
|
+
if key in rv:
|
|
36
|
+
current_value = rv[key]
|
|
37
|
+
if isinstance(current_value, list):
|
|
38
|
+
current_value.append(value)
|
|
39
|
+
else:
|
|
40
|
+
rv[key] = [current_value, value]
|
|
41
|
+
else:
|
|
42
|
+
rv[key] = value
|
|
43
|
+
return rv
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UserStoryChangeType(IntEnum):
|
|
47
|
+
APPEND = 1
|
|
48
|
+
REPLACE = 2
|
|
49
|
+
DELETE = 3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class UserStoryChange:
|
|
54
|
+
field: str
|
|
55
|
+
type: UserStoryChangeType
|
|
56
|
+
# In case of replace changes, the current value to replace or delete
|
|
57
|
+
old_value: Optional[str] = None
|
|
58
|
+
# In case of append or replace changes the new value to set
|
|
59
|
+
new_value: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def append(cls, field: str, new_value: str) -> Self:
|
|
63
|
+
return cls(field, UserStoryChangeType.APPEND, None, new_value)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def replace(cls, field: str, old_value: str, new_value: str) -> Self:
|
|
67
|
+
return cls(field, UserStoryChangeType.REPLACE, old_value, new_value)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def delete(cls, field: str, old_value: str) -> Self:
|
|
71
|
+
return cls(field, UserStoryChangeType.DELETE, old_value, None)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def update(user_story: str, changes: list[UserStoryChange]) -> Optional[str]:
|
|
75
|
+
new_user_story = []
|
|
76
|
+
user_story_updates = defaultdict(list)
|
|
77
|
+
for change in changes:
|
|
78
|
+
user_story_updates[change.field].append(change)
|
|
79
|
+
|
|
80
|
+
has_updates = False
|
|
81
|
+
applied_changes = set()
|
|
82
|
+
|
|
83
|
+
for line, key, value in parse(user_story):
|
|
84
|
+
if key is None or value is None:
|
|
85
|
+
new_user_story.append(line)
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
output_line: Optional[tuple[str, str]] = (key, value)
|
|
89
|
+
if key in user_story_updates:
|
|
90
|
+
changes = user_story_updates[key]
|
|
91
|
+
current_value = value.strip()
|
|
92
|
+
for change in changes:
|
|
93
|
+
if change in applied_changes:
|
|
94
|
+
continue
|
|
95
|
+
if current_value == change.old_value:
|
|
96
|
+
applied_changes.add(change)
|
|
97
|
+
if change.type == UserStoryChangeType.DELETE:
|
|
98
|
+
output_line = None
|
|
99
|
+
has_updates = True
|
|
100
|
+
elif change.type == UserStoryChangeType.REPLACE:
|
|
101
|
+
assert change.new_value is not None
|
|
102
|
+
output_line = (key, change.new_value)
|
|
103
|
+
has_updates = True
|
|
104
|
+
elif (
|
|
105
|
+
change.type == UserStoryChangeType.APPEND
|
|
106
|
+
and current_value == change.new_value
|
|
107
|
+
):
|
|
108
|
+
# If we are going to append a value that's already there
|
|
109
|
+
# do nothing
|
|
110
|
+
applied_changes.add(change)
|
|
111
|
+
if output_line is not None:
|
|
112
|
+
new_user_story.append(f"{output_line[0]}:{output_line[1]}")
|
|
113
|
+
|
|
114
|
+
for changes in user_story_updates.values():
|
|
115
|
+
for change in changes:
|
|
116
|
+
if change not in applied_changes:
|
|
117
|
+
if change.type == UserStoryChangeType.DELETE:
|
|
118
|
+
# Tried to delete a key that doesn't exist, do nothing
|
|
119
|
+
pass
|
|
120
|
+
elif change.type == UserStoryChangeType.REPLACE:
|
|
121
|
+
# Tried to replace a key that doesn't exist, do nothing
|
|
122
|
+
pass
|
|
123
|
+
elif change.type == UserStoryChangeType.APPEND:
|
|
124
|
+
new_user_story.append(f"{change.field}:{change.new_value}")
|
|
125
|
+
has_updates = True
|
|
126
|
+
|
|
127
|
+
if has_updates:
|
|
128
|
+
return "\n".join(new_user_story) + "\n"
|
|
129
|
+
|
|
130
|
+
return None
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugdantic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Bugzilla client based on pydantic
|
|
5
5
|
Author-email: James Graham <james@hoppipolla.co.uk>
|
|
6
|
-
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
7
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
8
8
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
9
9
|
Requires-Python: >=3.11
|
|
@@ -4,9 +4,11 @@ pyproject.toml
|
|
|
4
4
|
bugdantic/__init__.py
|
|
5
5
|
bugdantic/bugzilla.py
|
|
6
6
|
bugdantic/py.typed
|
|
7
|
+
bugdantic/userstory.py
|
|
7
8
|
bugdantic.egg-info/PKG-INFO
|
|
8
9
|
bugdantic.egg-info/SOURCES.txt
|
|
9
10
|
bugdantic.egg-info/dependency_links.txt
|
|
10
11
|
bugdantic.egg-info/requires.txt
|
|
11
12
|
bugdantic.egg-info/top_level.txt
|
|
12
|
-
test/test_bugzilla.py
|
|
13
|
+
test/test_bugzilla.py
|
|
14
|
+
test/test_userstory.py
|
|
@@ -5,10 +5,10 @@ authors = [
|
|
|
5
5
|
{name = "James Graham", email = "james@hoppipolla.co.uk"}
|
|
6
6
|
]
|
|
7
7
|
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.3.0"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
|
+
license = "MPL-2.0"
|
|
10
11
|
classifiers = [
|
|
11
|
-
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
|
12
12
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
13
13
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
14
14
|
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from bugdantic import userstory
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_parse_user_story():
|
|
5
|
+
user_story = """
|
|
6
|
+
foo:bar
|
|
7
|
+
abcde:
|
|
8
|
+
foo : baz
|
|
9
|
+
some long line: with a colon
|
|
10
|
+
key-:value
|
|
11
|
+
"""
|
|
12
|
+
assert list(userstory.parse(user_story)) == [
|
|
13
|
+
("", None, None),
|
|
14
|
+
("foo:bar", "foo", "bar"),
|
|
15
|
+
("abcde:", None, None),
|
|
16
|
+
(" foo : baz", "foo", "baz"),
|
|
17
|
+
("some long line: with a colon", None, None),
|
|
18
|
+
("key-:value", "key-", "value"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
assert userstory.parse_as_dict(user_story) == {
|
|
22
|
+
"foo": ["bar", "baz"],
|
|
23
|
+
"key-": "value",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_update_user_story():
|
|
28
|
+
user_story = """
|
|
29
|
+
foo:bar
|
|
30
|
+
abcde:
|
|
31
|
+
foo : baz
|
|
32
|
+
some long line: with a colon
|
|
33
|
+
key-:value
|
|
34
|
+
key:value
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
changes = [
|
|
38
|
+
userstory.UserStoryChange.replace("foo", "bar", "foobar"),
|
|
39
|
+
userstory.UserStoryChange.delete("key-", "value"),
|
|
40
|
+
userstory.UserStoryChange.append("foo", "new"),
|
|
41
|
+
userstory.UserStoryChange.append("new_key", "new_value"),
|
|
42
|
+
# These ones shouldn't do anything
|
|
43
|
+
userstory.UserStoryChange.replace("foo", "missing", "fail"),
|
|
44
|
+
userstory.UserStoryChange.replace("missing", "missing", "fail"),
|
|
45
|
+
userstory.UserStoryChange.delete("key", "missing"),
|
|
46
|
+
userstory.UserStoryChange.delete("missing", "missing"),
|
|
47
|
+
]
|
|
48
|
+
assert (
|
|
49
|
+
userstory.update(user_story, changes)
|
|
50
|
+
== """
|
|
51
|
+
foo:foobar
|
|
52
|
+
abcde:
|
|
53
|
+
foo:baz
|
|
54
|
+
some long line: with a colon
|
|
55
|
+
key:value
|
|
56
|
+
foo:new
|
|
57
|
+
new_key:new_value
|
|
58
|
+
"""
|
|
59
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|