cli2 5.1.6__tar.gz → 5.1.8__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.
- {cli2-5.1.6/cli2.egg-info → cli2-5.1.8}/PKG-INFO +1 -1
- {cli2-5.1.6 → cli2-5.1.8}/cli2/__init__.py +4 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/cli.py +17 -5
- {cli2-5.1.6 → cli2-5.1.8}/cli2/configuration.py +17 -1
- cli2-5.1.8/cli2/interactive.py +80 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/log.py +17 -10
- cli2-5.1.8/cli2/notlevenshtein.py +74 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/table.py +1 -1
- {cli2-5.1.6 → cli2-5.1.8}/cli2/test.py +3 -3
- cli2-5.1.8/cli2/theme.py +223 -0
- {cli2-5.1.6 → cli2-5.1.8/cli2.egg-info}/PKG-INFO +1 -1
- {cli2-5.1.6 → cli2-5.1.8}/cli2.egg-info/SOURCES.txt +5 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2.egg-info/entry_points.txt +2 -0
- {cli2-5.1.6 → cli2-5.1.8}/setup.py +3 -1
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_ansible.py +1 -1
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_cli.py +3 -3
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_client.py +6 -5
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_command.py +5 -4
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_configuration.py +3 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_group.py +14 -6
- cli2-5.1.8/tests/test_interactive.py +50 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_lock.py +1 -1
- cli2-5.1.8/tests/test_notlevenshtein.py +46 -0
- {cli2-5.1.6 → cli2-5.1.8}/MANIFEST.in +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/README.rst +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/classifiers.txt +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/asyncio.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/cli2.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/colors.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/decorators.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/display.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/__init__.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/conf.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/example.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/example_obj.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/nesting.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/obj.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/obj2.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/examples/test.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/lock.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/mask.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/node.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2/sphinx.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2.egg-info/dependency_links.txt +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2.egg-info/requires.txt +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/cli2.egg-info/top_level.txt +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/setup.cfg +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_ansible_variables.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_asyncio.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_client_test.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_decorators.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_display.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_entry_point.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_inject.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_log.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_mask.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_node.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_restful.py +0 -0
- {cli2-5.1.6 → cli2-5.1.8}/tests/test_table.py +0 -0
|
@@ -11,17 +11,21 @@ from .cli import (
|
|
|
11
11
|
)
|
|
12
12
|
from .asyncio import async_resolve, async_run, Queue
|
|
13
13
|
from .colors import colors as c
|
|
14
|
+
from .theme import theme, t
|
|
14
15
|
|
|
15
16
|
from .configuration import Configuration, cfg
|
|
16
17
|
from .display import diff, diff_data, render, print, highlight, yaml_highlight
|
|
18
|
+
from .interactive import choice, editor
|
|
17
19
|
try:
|
|
18
20
|
import fcntl
|
|
19
21
|
except ImportError:
|
|
20
22
|
""" windows """
|
|
21
23
|
else:
|
|
22
24
|
from .lock import Lock
|
|
25
|
+
|
|
23
26
|
from .log import configure, log, parse
|
|
24
27
|
from .mask import Mask
|
|
28
|
+
from .notlevenshtein import closest, closest_path
|
|
25
29
|
from .table import Table
|
|
26
30
|
|
|
27
31
|
|
|
@@ -299,6 +299,9 @@ class Group(EntryPoint, dict):
|
|
|
299
299
|
continue
|
|
300
300
|
if leaf and getattr(final, name, '_') is None:
|
|
301
301
|
continue
|
|
302
|
+
if isinstance(method, classmethod):
|
|
303
|
+
# get the bound method
|
|
304
|
+
method = getattr(cls, name)
|
|
302
305
|
self.load_method(final, method)
|
|
303
306
|
|
|
304
307
|
def load_obj(self, obj):
|
|
@@ -308,9 +311,10 @@ class Group(EntryPoint, dict):
|
|
|
308
311
|
for name in dir(obj):
|
|
309
312
|
if name.startswith('_'):
|
|
310
313
|
continue
|
|
311
|
-
if not callable(getattr(type(obj), name)):
|
|
314
|
+
if not callable(getattr(type(obj), name, None)):
|
|
312
315
|
continue
|
|
313
|
-
|
|
316
|
+
method = getattr(obj, name)
|
|
317
|
+
self.load_method(obj, method)
|
|
314
318
|
|
|
315
319
|
def load_method(self, obj, method):
|
|
316
320
|
wrapped_method = getattr(method, '__func__', None)
|
|
@@ -325,7 +329,7 @@ class Group(EntryPoint, dict):
|
|
|
325
329
|
if condition:
|
|
326
330
|
if not condition(obj):
|
|
327
331
|
return
|
|
328
|
-
self.cmd(
|
|
332
|
+
self.cmd(method)
|
|
329
333
|
|
|
330
334
|
def __call__(self, *argv):
|
|
331
335
|
self.exit_code = 0
|
|
@@ -825,7 +829,11 @@ class Argument:
|
|
|
825
829
|
self.type = param.annotation
|
|
826
830
|
|
|
827
831
|
self.negate = None
|
|
828
|
-
if
|
|
832
|
+
if (
|
|
833
|
+
self.iskw
|
|
834
|
+
and self.param.annotation == bool
|
|
835
|
+
and self.param.default is not False
|
|
836
|
+
):
|
|
829
837
|
self.negate = 'no-' + param.name
|
|
830
838
|
if cmd.posix:
|
|
831
839
|
self.negate = self.negate.replace('_', '-')
|
|
@@ -978,7 +986,11 @@ class Argument:
|
|
|
978
986
|
+ colors.reset
|
|
979
987
|
)
|
|
980
988
|
|
|
981
|
-
if
|
|
989
|
+
if (
|
|
990
|
+
self.type == bool
|
|
991
|
+
and not self.negates
|
|
992
|
+
and self.param.default is not False
|
|
993
|
+
):
|
|
982
994
|
self.cmd.print(
|
|
983
995
|
'Accepted: '
|
|
984
996
|
+ colors.blue3
|
|
@@ -8,6 +8,8 @@ The developer story we are after:
|
|
|
8
8
|
- when there's a minute to be nice to the user, add some help that will be
|
|
9
9
|
displayed to them: ``cli2.cfg.questions['YOUR_ENV_VAR'] = 'this is a help
|
|
10
10
|
text that will be display to the user when we prompt them for YOUR_ENV_VAR'``
|
|
11
|
+
- you can also set ``cli2.cfg.defaults['YOUR_ENV_VAR']`` if you prefer a
|
|
12
|
+
default value to an interactive prompt
|
|
11
13
|
|
|
12
14
|
This is the user story we are after:
|
|
13
15
|
|
|
@@ -29,6 +31,8 @@ import shlex
|
|
|
29
31
|
import textwrap
|
|
30
32
|
from pathlib import Path
|
|
31
33
|
|
|
34
|
+
from .log import log
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
class Configuration(dict):
|
|
34
38
|
"""
|
|
@@ -43,6 +47,12 @@ class Configuration(dict):
|
|
|
43
47
|
configuration then question_string will be used as text to prompt the
|
|
44
48
|
user.
|
|
45
49
|
|
|
50
|
+
.. py:attribute:: defaults
|
|
51
|
+
|
|
52
|
+
A dict of ``ENV_VAR='default_value'``, if an env var is missing from
|
|
53
|
+
configuration then the default value will be returned instead of
|
|
54
|
+
displaying an interactive prompt.
|
|
55
|
+
|
|
46
56
|
.. py:attribute:: profile_path
|
|
47
57
|
|
|
48
58
|
Path to the shell profile to save/read variables, defaults to
|
|
@@ -67,8 +77,9 @@ class Configuration(dict):
|
|
|
67
77
|
api_url = cli2.cfg['USERNAME']
|
|
68
78
|
|
|
69
79
|
"""
|
|
70
|
-
def __init__(self, profile_path=None, **questions):
|
|
80
|
+
def __init__(self, profile_path=None, defaults=None, **questions):
|
|
71
81
|
self.questions = questions
|
|
82
|
+
self.defaults = defaults or dict()
|
|
72
83
|
self.profile_path = Path(
|
|
73
84
|
profile_path or os.getenv('HOME') + '/.profile'
|
|
74
85
|
)
|
|
@@ -155,6 +166,11 @@ class Configuration(dict):
|
|
|
155
166
|
if key in self.profile_variables:
|
|
156
167
|
return self.profile_variables[key]
|
|
157
168
|
|
|
169
|
+
if key in self.defaults:
|
|
170
|
+
value = self.defaults[key]
|
|
171
|
+
log.debug(f'Defaulting {key} to {value}')
|
|
172
|
+
return value
|
|
173
|
+
|
|
158
174
|
prompt = self.questions.get(key, key)
|
|
159
175
|
prompt = textwrap.dedent(prompt).strip()
|
|
160
176
|
value = self.input(prompt)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
""" Interactive user inputs """
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from .log import log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def choice(question, choices=None, default=None):
|
|
11
|
+
"""
|
|
12
|
+
Ask user to make a choice.
|
|
13
|
+
|
|
14
|
+
.. code-block::
|
|
15
|
+
|
|
16
|
+
accepted = cli2.choice('Accept terms?') == 'y'
|
|
17
|
+
|
|
18
|
+
:param question: String question to ask
|
|
19
|
+
:param choices: List of acceptable choices, y/n by default
|
|
20
|
+
:param default: Default value for when the user does not add a value.
|
|
21
|
+
"""
|
|
22
|
+
choices = [c.lower() for c in choices or ['y', 'n']]
|
|
23
|
+
|
|
24
|
+
if default:
|
|
25
|
+
choices_display = [
|
|
26
|
+
c.upper() if c == default.lower() else c
|
|
27
|
+
for c in choices
|
|
28
|
+
]
|
|
29
|
+
else:
|
|
30
|
+
choices_display = choices
|
|
31
|
+
|
|
32
|
+
question = question + f' ({"/".join(choices_display)})'
|
|
33
|
+
|
|
34
|
+
tries = 30
|
|
35
|
+
while tries:
|
|
36
|
+
answer = input(question)
|
|
37
|
+
if not answer and default:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
if answer.lower() in choices:
|
|
41
|
+
return answer.lower()
|
|
42
|
+
|
|
43
|
+
tries -= 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def editor(content=None):
|
|
47
|
+
"""
|
|
48
|
+
Open $EDITOR with content, return the result.
|
|
49
|
+
|
|
50
|
+
Like git rebase -i does!
|
|
51
|
+
|
|
52
|
+
:param content: Initial content if any
|
|
53
|
+
:return: The edited content after $EDITOR exit
|
|
54
|
+
"""
|
|
55
|
+
tmp = tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix=".txt")
|
|
56
|
+
with tmp as f:
|
|
57
|
+
f.write(content)
|
|
58
|
+
f.flush()
|
|
59
|
+
filepath = f.name
|
|
60
|
+
|
|
61
|
+
editor = os.getenv('EDITOR', 'vim')
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
command = f"{editor} {shlex.quote(filepath)}"
|
|
65
|
+
subprocess.run(shlex.split(command), check=True)
|
|
66
|
+
|
|
67
|
+
with open(filepath, 'r') as f:
|
|
68
|
+
content = f.read()
|
|
69
|
+
return content
|
|
70
|
+
except subprocess.CalledProcessError as e:
|
|
71
|
+
log.error(f"Error running Vim: {e}")
|
|
72
|
+
return None
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
log.warn(f"Temporary file gone?? {filepath}")
|
|
75
|
+
return None
|
|
76
|
+
finally:
|
|
77
|
+
try:
|
|
78
|
+
os.remove(filepath)
|
|
79
|
+
except OSError as e:
|
|
80
|
+
log.warn(f"Error deleting temporary file {filepath}: {e}")
|
|
@@ -94,7 +94,6 @@ def configure(log_file=None):
|
|
|
94
94
|
if os.getenv('DEBUG'):
|
|
95
95
|
LOG_LEVEL = 'DEBUG'
|
|
96
96
|
|
|
97
|
-
timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S')
|
|
98
97
|
pre_chain = [
|
|
99
98
|
# add log level and timestamp to event_dict
|
|
100
99
|
structlog.stdlib.add_log_level,
|
|
@@ -102,9 +101,12 @@ def configure(log_file=None):
|
|
|
102
101
|
# that values in the extra parameters of log methods pass through to
|
|
103
102
|
# log output
|
|
104
103
|
structlog.stdlib.ExtraAdder(),
|
|
105
|
-
timestamper,
|
|
106
104
|
]
|
|
107
105
|
|
|
106
|
+
if 'NO_TIMESTAMPER' not in os.environ:
|
|
107
|
+
timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S')
|
|
108
|
+
pre_chain.append(timestamper)
|
|
109
|
+
|
|
108
110
|
cmd = '_'.join([
|
|
109
111
|
re.sub('[^0-9a-zA-Z]+', '_', arg.split('/')[-1])
|
|
110
112
|
for arg in sys.argv
|
|
@@ -213,18 +215,23 @@ def configure(log_file=None):
|
|
|
213
215
|
|
|
214
216
|
logging.config.dictConfig(LOGGING)
|
|
215
217
|
|
|
218
|
+
processors = [
|
|
219
|
+
structlog.stdlib.add_log_level,
|
|
220
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
221
|
+
]
|
|
222
|
+
if 'NO_TIMESTAMPER' not in os.environ:
|
|
223
|
+
processors.append(timestamper)
|
|
224
|
+
processors += [
|
|
225
|
+
structlog.processors.StackInfoRenderer(),
|
|
226
|
+
structlog.processors.format_exc_info,
|
|
227
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
228
|
+
]
|
|
229
|
+
|
|
216
230
|
structlog.configure(
|
|
217
231
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
218
232
|
wrapper_class=structlog.stdlib.BoundLogger,
|
|
219
233
|
cache_logger_on_first_use=True,
|
|
220
|
-
processors=
|
|
221
|
-
structlog.stdlib.add_log_level,
|
|
222
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
223
|
-
timestamper,
|
|
224
|
-
structlog.processors.StackInfoRenderer(),
|
|
225
|
-
structlog.processors.format_exc_info,
|
|
226
|
-
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
227
|
-
],
|
|
234
|
+
processors=processors,
|
|
228
235
|
)
|
|
229
236
|
|
|
230
237
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Like levenshtein with difflib.
|
|
3
|
+
|
|
4
|
+
.. code-block:: python
|
|
5
|
+
|
|
6
|
+
source_word = "apple"
|
|
7
|
+
word_list = ["apply", "aple", "banana", "orange", "applet"]
|
|
8
|
+
|
|
9
|
+
closest = cli2.closest(source_word, word_list)
|
|
10
|
+
print(f"The closest word to '{source_word}' is: {closest}")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import difflib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def closest(source_token, token_list):
|
|
17
|
+
"""
|
|
18
|
+
Finds the token in token_list with the shortest distance to source_token.
|
|
19
|
+
|
|
20
|
+
:param source_token: The source token (string).
|
|
21
|
+
:param token_list: A list of tokens (strings).
|
|
22
|
+
:return: The token with the shortest distance, or None if token_list is
|
|
23
|
+
empty.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
if not token_list:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
closest_token = None
|
|
30
|
+
shortest_distance = float("inf") # Initialize with infinity
|
|
31
|
+
|
|
32
|
+
for token in token_list:
|
|
33
|
+
matcher = difflib.SequenceMatcher(None, source_token, token)
|
|
34
|
+
distance = (
|
|
35
|
+
1 - matcher.ratio()
|
|
36
|
+
) # Calculate a distance metric (1 - similarity ratio)
|
|
37
|
+
|
|
38
|
+
if distance < shortest_distance:
|
|
39
|
+
shortest_distance = distance
|
|
40
|
+
closest_token = token
|
|
41
|
+
|
|
42
|
+
return closest_token
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def closest_path(path, paths):
|
|
46
|
+
"""
|
|
47
|
+
Find the closest path from paths.
|
|
48
|
+
|
|
49
|
+
LLM may output broken paths, this fixes them.
|
|
50
|
+
|
|
51
|
+
:param path: Path to find closest
|
|
52
|
+
:param paths: List of paths to search in.
|
|
53
|
+
"""
|
|
54
|
+
parts = path.split('/')
|
|
55
|
+
for number, part in enumerate(parts):
|
|
56
|
+
paths_parts = {
|
|
57
|
+
str(path).split('/')[number]
|
|
58
|
+
for path in paths
|
|
59
|
+
}
|
|
60
|
+
if part not in paths_parts:
|
|
61
|
+
path_part = closest(part, paths_parts)
|
|
62
|
+
if not path_part:
|
|
63
|
+
return None # not found at all
|
|
64
|
+
else:
|
|
65
|
+
path_part = part
|
|
66
|
+
|
|
67
|
+
parts[number] = path_part
|
|
68
|
+
paths = {
|
|
69
|
+
path
|
|
70
|
+
for path in paths
|
|
71
|
+
if str(path).startswith('/'.join(parts[:number + 1]))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return '/'.join(parts)
|
|
@@ -184,7 +184,7 @@ class Table(list):
|
|
|
184
184
|
if len(wrapped) > 1:
|
|
185
185
|
leftovers[colnum] = ' '.join(wrapped[1:])
|
|
186
186
|
if color:
|
|
187
|
-
line[-1] = color + line[-1] + colors.reset
|
|
187
|
+
line[-1] = str(color) + line[-1] + colors.reset
|
|
188
188
|
for leftover in leftovers:
|
|
189
189
|
if len(leftover):
|
|
190
190
|
rows.insert(0, leftovers)
|
|
@@ -75,7 +75,7 @@ def autotest(path, cmd, ignore=None, env=None):
|
|
|
75
75
|
'''.strip(),
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
diff_cmd = 'diff -U 1
|
|
78
|
+
diff_cmd = 'diff -U 1 "%s" - | sed "1,2 d"' % path
|
|
79
79
|
proc = subprocess.Popen(
|
|
80
80
|
diff_cmd,
|
|
81
81
|
stdout=subprocess.PIPE,
|
|
@@ -88,8 +88,8 @@ def autotest(path, cmd, ignore=None, env=None):
|
|
|
88
88
|
if diff_out:
|
|
89
89
|
raise type(f'''
|
|
90
90
|
DiffFound
|
|
91
|
-
- {
|
|
92
|
-
+ {
|
|
91
|
+
- {path}
|
|
92
|
+
+ {cmd}
|
|
93
93
|
'''.strip(), (Exception,), {})('\n' + diff_out.decode('utf8'))
|
|
94
94
|
|
|
95
95
|
|
cli2-5.1.8/cli2/theme.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Color themes.
|
|
3
|
+
|
|
4
|
+
The theme is available in the `cli2.t` namespace.
|
|
5
|
+
|
|
6
|
+
.. envar:: CLI2_THEME
|
|
7
|
+
|
|
8
|
+
The default is "standard" but "monokai" and "flashy" are also available.
|
|
9
|
+
Standard theme uses basic colors, and lets you override them with
|
|
10
|
+
environment variables: CLI2_RED, CLI2_GREEN, and so on.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
.. code-block:: python
|
|
15
|
+
|
|
16
|
+
import cli2
|
|
17
|
+
|
|
18
|
+
print(f'{cli2.theme.green}OK{cli2.theme.reset}')
|
|
19
|
+
|
|
20
|
+
# with a mode, such as bold, dim, italic, underline and strike:
|
|
21
|
+
print(f'{cli2.theme.green.bold}OK{cli2.theme.reset}')
|
|
22
|
+
|
|
23
|
+
# all is also callable appending the reset automatically
|
|
24
|
+
print(cli2.theme.green('OK'))
|
|
25
|
+
print(cli2.theme.green.bold('OK'))
|
|
26
|
+
|
|
27
|
+
We also have shortcuts, ``cli2.theme`` is ``cli2.t``, each color can be
|
|
28
|
+
referred to by first letter in lowercase, except for black and gray which are
|
|
29
|
+
refered to by their first letter in uppercase. Modes can be referred to by
|
|
30
|
+
first letter in lowercase too, and reset is rs:
|
|
31
|
+
|
|
32
|
+
.. code-block:: python
|
|
33
|
+
|
|
34
|
+
# shortcuts
|
|
35
|
+
print(f'{cli2.t.g.b}OK{cli2.t.rs}')
|
|
36
|
+
|
|
37
|
+
# shorter with callable
|
|
38
|
+
print(cli2.t.g.b('OK'))
|
|
39
|
+
|
|
40
|
+
Run ``cli2-theme`` for the list of colors by theme.
|
|
41
|
+
"""
|
|
42
|
+
import re
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
from .cli import Command
|
|
46
|
+
|
|
47
|
+
themes = dict(
|
|
48
|
+
standard=dict(
|
|
49
|
+
black=int(os.getenv('CLI2_BLACK', 0)),
|
|
50
|
+
red=int(os.getenv('CLI2_RED', 1)),
|
|
51
|
+
green=int(os.getenv('CLI2_GREEN', 2)),
|
|
52
|
+
yellow=int(os.getenv('CLI2_YELLOW', 3)),
|
|
53
|
+
orange=int(os.getenv('CLI2_ORANGE', 208)),
|
|
54
|
+
blue=int(os.getenv('CLI2_BLUE', 4)),
|
|
55
|
+
mauve=int(os.getenv('CLI2_MAUVE', 5)),
|
|
56
|
+
pink=int(os.getenv('CLI2_PINK', 164)),
|
|
57
|
+
cyan=int(os.getenv('CLI2_CYAN', 6)),
|
|
58
|
+
gray=int(os.getenv('CLI2_GRAY', 7)),
|
|
59
|
+
),
|
|
60
|
+
flashy=dict(
|
|
61
|
+
black=0,
|
|
62
|
+
red=196,
|
|
63
|
+
green=46,
|
|
64
|
+
yellow=227,
|
|
65
|
+
blue=27,
|
|
66
|
+
mauve=129,
|
|
67
|
+
pink=201,
|
|
68
|
+
orange=202,
|
|
69
|
+
cyan=51,
|
|
70
|
+
gray=253,
|
|
71
|
+
),
|
|
72
|
+
monokai=dict(
|
|
73
|
+
black=0,
|
|
74
|
+
red=124,
|
|
75
|
+
green=150,
|
|
76
|
+
yellow=179,
|
|
77
|
+
blue=67,
|
|
78
|
+
mauve=140,
|
|
79
|
+
pink=132,
|
|
80
|
+
orange=202,
|
|
81
|
+
cyan=80,
|
|
82
|
+
gray=246,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Renderer:
|
|
88
|
+
def __call__(self, *content):
|
|
89
|
+
return f'{self}{" ".join([str(c) for c in content])}{t.rs}'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Mode(Renderer):
|
|
93
|
+
def __init__(self, name, code):
|
|
94
|
+
self.name = name
|
|
95
|
+
self.code = code
|
|
96
|
+
self.alias = name[0]
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
return f'\u001b[{self.code}m'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ColorMode(Renderer):
|
|
103
|
+
def __init__(self, color, mode):
|
|
104
|
+
self.color = color
|
|
105
|
+
self.mode = mode
|
|
106
|
+
|
|
107
|
+
def __str__(self):
|
|
108
|
+
return f'\u001b[{self.mode.code};38;5;{self.color.code}m'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
modes = {
|
|
112
|
+
name: Mode(name, code)
|
|
113
|
+
for name, code in dict(
|
|
114
|
+
bold=1,
|
|
115
|
+
dim=2,
|
|
116
|
+
italic=3,
|
|
117
|
+
underline=4,
|
|
118
|
+
strike=9,
|
|
119
|
+
).items()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Color(Renderer):
|
|
124
|
+
def __init__(self, code, name=None, alias=None):
|
|
125
|
+
self.name = name
|
|
126
|
+
self.alias = alias
|
|
127
|
+
self.code = code
|
|
128
|
+
|
|
129
|
+
for mode, code in modes.items():
|
|
130
|
+
color_mode = ColorMode(self, code)
|
|
131
|
+
setattr(self, mode, color_mode)
|
|
132
|
+
setattr(self, mode[0], color_mode)
|
|
133
|
+
|
|
134
|
+
def __str__(self):
|
|
135
|
+
return f'\u001b[38;5;{self.code}m'
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Theme:
|
|
139
|
+
def __init__(self, colors):
|
|
140
|
+
self.colors = colors
|
|
141
|
+
|
|
142
|
+
for name, mode in modes.items():
|
|
143
|
+
setattr(self, mode.name, mode)
|
|
144
|
+
|
|
145
|
+
for name, value in colors.items():
|
|
146
|
+
if name in ('black', 'gray'):
|
|
147
|
+
alias = name[0].upper()
|
|
148
|
+
else:
|
|
149
|
+
alias = name[0]
|
|
150
|
+
|
|
151
|
+
color = Color(value, name, alias)
|
|
152
|
+
setattr(self, name, color)
|
|
153
|
+
setattr(self, alias, color)
|
|
154
|
+
|
|
155
|
+
for name in ('reset', 'rs'):
|
|
156
|
+
setattr(self, name, '\u001b[0m')
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def len(string):
|
|
160
|
+
"""
|
|
161
|
+
Counts the number of alphabetic characters in a string,
|
|
162
|
+
ignoring ANSI escape codes.
|
|
163
|
+
|
|
164
|
+
:param string: Input string
|
|
165
|
+
:return: Integer count of actual printable chars
|
|
166
|
+
"""
|
|
167
|
+
# Regular expression to match ANSI escape codes
|
|
168
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
169
|
+
|
|
170
|
+
# Remove ANSI escape codes from the string
|
|
171
|
+
cleaned_text = ansi_escape.sub('', string)
|
|
172
|
+
|
|
173
|
+
# Count the alphabetic characters in the cleaned string
|
|
174
|
+
letter_count = 0
|
|
175
|
+
for char in cleaned_text:
|
|
176
|
+
if 'a' <= char <= 'z' or 'A' <= char <= 'Z':
|
|
177
|
+
letter_count += 1
|
|
178
|
+
|
|
179
|
+
return letter_count
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
t = theme = Theme(themes[os.getenv('CLI2_THEME', 'standard')])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def demo():
|
|
186
|
+
"""
|
|
187
|
+
Print all colors and modes from the theme.
|
|
188
|
+
"""
|
|
189
|
+
for name, theme in themes.items():
|
|
190
|
+
theme = Theme(themes[name])
|
|
191
|
+
print(f'\n\nTheme: {theme.bold(name)}')
|
|
192
|
+
_demo(theme)
|
|
193
|
+
|
|
194
|
+
print()
|
|
195
|
+
print(theme.bold('MODES:'))
|
|
196
|
+
for name in modes:
|
|
197
|
+
mode = getattr(theme, name)
|
|
198
|
+
print(f'{mode}t.{name}{t.rs}')
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _demo(theme):
|
|
202
|
+
def color_data(alias, color):
|
|
203
|
+
data = [
|
|
204
|
+
(color, alias),
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for mode in modes:
|
|
208
|
+
data.append(
|
|
209
|
+
(getattr(color, mode[0]), f'{alias}.{mode[0]}'),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
from .table import Table
|
|
215
|
+
table = Table()
|
|
216
|
+
for name in t.colors:
|
|
217
|
+
color = getattr(theme, name)
|
|
218
|
+
|
|
219
|
+
table.append(color_data(color.alias, color))
|
|
220
|
+
table.print()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
cli = Command(demo)
|
|
@@ -10,13 +10,16 @@ cli2/colors.py
|
|
|
10
10
|
cli2/configuration.py
|
|
11
11
|
cli2/decorators.py
|
|
12
12
|
cli2/display.py
|
|
13
|
+
cli2/interactive.py
|
|
13
14
|
cli2/lock.py
|
|
14
15
|
cli2/log.py
|
|
15
16
|
cli2/mask.py
|
|
16
17
|
cli2/node.py
|
|
18
|
+
cli2/notlevenshtein.py
|
|
17
19
|
cli2/sphinx.py
|
|
18
20
|
cli2/table.py
|
|
19
21
|
cli2/test.py
|
|
22
|
+
cli2/theme.py
|
|
20
23
|
cli2.egg-info/PKG-INFO
|
|
21
24
|
cli2.egg-info/SOURCES.txt
|
|
22
25
|
cli2.egg-info/dependency_links.txt
|
|
@@ -44,9 +47,11 @@ tests/test_display.py
|
|
|
44
47
|
tests/test_entry_point.py
|
|
45
48
|
tests/test_group.py
|
|
46
49
|
tests/test_inject.py
|
|
50
|
+
tests/test_interactive.py
|
|
47
51
|
tests/test_lock.py
|
|
48
52
|
tests/test_log.py
|
|
49
53
|
tests/test_mask.py
|
|
50
54
|
tests/test_node.py
|
|
55
|
+
tests/test_notlevenshtein.py
|
|
51
56
|
tests/test_restful.py
|
|
52
57
|
tests/test_table.py
|
|
@@ -4,3 +4,5 @@ cli2-example = cli2.examples.obj:cli.entry_point
|
|
|
4
4
|
cli2-example-client = cli2.examples.client:cli.entry_point
|
|
5
5
|
cli2-example-nesting = cli2.examples.nesting:cli.entry_point
|
|
6
6
|
cli2-example2 = cli2.examples.obj2:cli.entry_point
|
|
7
|
+
cli2-md = cli2.markdown:cli.entry_point
|
|
8
|
+
cli2-theme = cli2.theme:cli.entry_point
|
|
@@ -44,7 +44,7 @@ from setuptools import setup
|
|
|
44
44
|
|
|
45
45
|
setup(
|
|
46
46
|
name='cli2',
|
|
47
|
-
version='5.1.
|
|
47
|
+
version='5.1.8',
|
|
48
48
|
setup_requires='setupmeta',
|
|
49
49
|
packages=['cli2'],
|
|
50
50
|
install_requires=[
|
|
@@ -75,6 +75,8 @@ setup(
|
|
|
75
75
|
entry_points={
|
|
76
76
|
'console_scripts': [
|
|
77
77
|
'cli2 = cli2.cli2:main.entry_point',
|
|
78
|
+
'cli2-theme = cli2.theme:cli.entry_point',
|
|
79
|
+
'cli2-md = cli2.markdown:cli.entry_point',
|
|
78
80
|
'cli2-example = cli2.examples.obj:cli.entry_point',
|
|
79
81
|
'cli2-example2 = cli2.examples.obj2:cli.entry_point',
|
|
80
82
|
'cli2-example-nesting = cli2.examples.nesting:cli.entry_point',
|
|
@@ -13,19 +13,19 @@ def test_call():
|
|
|
13
13
|
assert "args=('http://?x=bar',)" in result
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def test_doc_by_default(
|
|
16
|
+
def test_doc_by_default():
|
|
17
17
|
main.outfile = Outfile()
|
|
18
18
|
main('cli2.examples.test')
|
|
19
19
|
assert 'example_function' in main.outfile
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def test_help_argument(
|
|
22
|
+
def test_help_argument():
|
|
23
23
|
main.outfile = Outfile()
|
|
24
24
|
main('help', 'cli2.examples.test')
|
|
25
25
|
assert 'example_function' in main.outfile
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def test_help_no_argument(
|
|
28
|
+
def test_help_no_argument():
|
|
29
29
|
main.outfile = Outfile()
|
|
30
30
|
main()
|
|
31
31
|
assert 'help' in main.outfile
|
|
@@ -3,7 +3,7 @@ import cli2
|
|
|
3
3
|
import chttpx
|
|
4
4
|
import httpx
|
|
5
5
|
import inspect
|
|
6
|
-
import mock
|
|
6
|
+
from unittest import mock
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
|
|
@@ -304,7 +304,7 @@ async def test_handler(client_class):
|
|
|
304
304
|
response = httpx.Response(status_code=200)
|
|
305
305
|
response.request = httpx.Request('POST', '/', json=[1])
|
|
306
306
|
result = await handler(client, response, 0, log)
|
|
307
|
-
log.
|
|
307
|
+
log.warn.assert_called_once_with(
|
|
308
308
|
'retry', status_code=200, tries=0, sleep=.0
|
|
309
309
|
)
|
|
310
310
|
assert not result
|
|
@@ -313,7 +313,7 @@ async def test_handler(client_class):
|
|
|
313
313
|
response.request = httpx.Request('POST', '/', json=[1])
|
|
314
314
|
with pytest.raises(chttpx.RetriesExceededError) as exc:
|
|
315
315
|
await handler(client, response, handler.tries + 1, log)
|
|
316
|
-
log.
|
|
316
|
+
log.warn.assert_called_once_with(
|
|
317
317
|
'retry', status_code=200, tries=0, sleep=.0
|
|
318
318
|
)
|
|
319
319
|
|
|
@@ -347,8 +347,9 @@ async def test_handler(client_class):
|
|
|
347
347
|
assert not client.client_reset.await_count
|
|
348
348
|
exc = httpx.TransportError('foo')
|
|
349
349
|
exc.request = response.request
|
|
350
|
+
log.warn.reset_mock()
|
|
350
351
|
result = await handler(client, exc, 0, log)
|
|
351
|
-
log.warn.
|
|
352
|
+
log.warn.assert_called_with(
|
|
352
353
|
'reconnect',
|
|
353
354
|
error="TransportError('foo')",
|
|
354
355
|
method='POST',
|
|
@@ -366,7 +367,7 @@ async def test_handler(client_class):
|
|
|
366
367
|
assert not client.token_reset.await_count
|
|
367
368
|
log.warn.reset_mock()
|
|
368
369
|
result = await handler(client, response, 0, log)
|
|
369
|
-
|
|
370
|
+
assert mock.call('retoken') in log.warn.call_args_list
|
|
370
371
|
assert not result
|
|
371
372
|
assert client.token_reset.await_count == 1
|
|
372
373
|
|
|
@@ -4,6 +4,7 @@ import cli2.test
|
|
|
4
4
|
import inspect
|
|
5
5
|
import pytest
|
|
6
6
|
import os
|
|
7
|
+
from unittest import mock
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
os.environ['FORCE_COLOR'] = '1'
|
|
@@ -438,16 +439,16 @@ def test_docstring():
|
|
|
438
439
|
)
|
|
439
440
|
|
|
440
441
|
|
|
441
|
-
def test_print(
|
|
442
|
-
cmd = cli2.Command(lambda: True, outfile=
|
|
442
|
+
def test_print():
|
|
443
|
+
cmd = cli2.Command(lambda: True, outfile=mock.Mock())
|
|
443
444
|
cmd.print('orangebold', 'foo', 'bar')
|
|
444
445
|
assert cmd.outfile.write.call_args_list[0].args == (
|
|
445
446
|
'\x1b[1;38;5;202mfoo bar\x1b[0m',
|
|
446
447
|
)
|
|
447
448
|
|
|
448
449
|
|
|
449
|
-
def test_print_bold(
|
|
450
|
-
cmd = cli2.Command(lambda: True, outfile=
|
|
450
|
+
def test_print_bold():
|
|
451
|
+
cmd = cli2.Command(lambda: True, outfile=mock.Mock())
|
|
451
452
|
cmd.print('ORANGE', 'foo', 'bar')
|
|
452
453
|
assert cmd.outfile.write.call_args_list[0].args == (
|
|
453
454
|
'\x1b[1;38;5;202mfoo bar\x1b[0m',
|
|
@@ -195,9 +195,12 @@ def test_load():
|
|
|
195
195
|
raise Exception('fails')
|
|
196
196
|
|
|
197
197
|
class Bar(metaclass=BarMetaclass):
|
|
198
|
+
def __init__(self, init=None):
|
|
199
|
+
self.init = init
|
|
200
|
+
|
|
198
201
|
@cli2.cmd
|
|
199
202
|
def test(self):
|
|
200
|
-
|
|
203
|
+
return self.init
|
|
201
204
|
|
|
202
205
|
@classmethod
|
|
203
206
|
@cli2.cmd
|
|
@@ -228,11 +231,16 @@ def test_load():
|
|
|
228
231
|
group.load(Foo)
|
|
229
232
|
assert list(group.keys()) == ['help', 'test', 'classmeth', 'test2']
|
|
230
233
|
|
|
231
|
-
group = cli2.Group(
|
|
232
|
-
group.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
group = cli2.Group()
|
|
235
|
+
group['0'] = group.group('0')
|
|
236
|
+
group['1'] = group.group('1')
|
|
237
|
+
group['0'].load(Foo(0))
|
|
238
|
+
group['1'].load(Foo(1))
|
|
239
|
+
assert list(group['0'].keys()) == ['help', 'classmeth', 'test', 'test2']
|
|
240
|
+
|
|
241
|
+
assert isinstance(group['0']['test2'](), Foo)
|
|
242
|
+
assert group['1']['test']() == 1
|
|
243
|
+
assert group['0']['test']() == 0
|
|
236
244
|
|
|
237
245
|
class Child(Foo):
|
|
238
246
|
test2 = None
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import cli2
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_default_choices_y():
|
|
6
|
+
with mock.patch("builtins.input", return_value="y"):
|
|
7
|
+
result = cli2.choice("Test question")
|
|
8
|
+
assert result == "y"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_default_choices_n():
|
|
12
|
+
with mock.patch("builtins.input", return_value="n"):
|
|
13
|
+
result = cli2.choice("Test question")
|
|
14
|
+
assert result == "n"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_custom_choices():
|
|
18
|
+
with mock.patch("builtins.input", return_value="b"):
|
|
19
|
+
result = cli2.choice("Test question", choices=["a", "b", "c"])
|
|
20
|
+
assert result == "b"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_default_value_provided():
|
|
24
|
+
with mock.patch("builtins.input", return_value=""):
|
|
25
|
+
result = cli2.choice("Test question", choices=["a", "b", "c"], default="b")
|
|
26
|
+
assert result == "b"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_default_value_not_provided():
|
|
30
|
+
with mock.patch("builtins.input", return_value=""):
|
|
31
|
+
result = cli2.choice("Test question", choices=["a", "b", "c"])
|
|
32
|
+
assert result is None # because the while loop breaks without return.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_case_insensitive_input():
|
|
36
|
+
with mock.patch("builtins.input", return_value="A"):
|
|
37
|
+
result = cli2.choice("Test question", choices=["a", "b", "c"])
|
|
38
|
+
assert result == "a"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_invalid_input():
|
|
42
|
+
with mock.patch("builtins.input", side_effect=["d", "a"]):
|
|
43
|
+
result = cli2.choice("Test question", choices=["a", "b", "c"])
|
|
44
|
+
assert result == "a"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_empty_choices_defaults_to_yn():
|
|
48
|
+
with mock.patch("builtins.input", return_value="y"):
|
|
49
|
+
result = cli2.choice("Test question", choices=[])
|
|
50
|
+
assert result == "y"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import cli2
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_find_closest_token_basic():
|
|
6
|
+
source_word = "apple"
|
|
7
|
+
word_list = ["apply", "aple", "banana", "orange", "applet"]
|
|
8
|
+
assert cli2.closest(source_word, word_list) == "applet"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_find_closest_token_exact_match():
|
|
12
|
+
source_word = "cat"
|
|
13
|
+
word_list = ["dog", "cat", "bat"]
|
|
14
|
+
assert cli2.closest(source_word, word_list) == "cat"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_find_closest_token_multiple_close():
|
|
18
|
+
source_word = "car"
|
|
19
|
+
word_list = ["card", "cart", "care"]
|
|
20
|
+
assert cli2.closest(source_word, word_list) == "card"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_find_closest_token_empty_list():
|
|
24
|
+
source_word = "test"
|
|
25
|
+
word_list = []
|
|
26
|
+
assert cli2.closest(source_word, word_list) is None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_find_closest_token_no_close_match():
|
|
30
|
+
source_word = "xyz"
|
|
31
|
+
word_list = ["apple", "banana", "orange"]
|
|
32
|
+
# difflib will pick the first if no close matches.
|
|
33
|
+
assert cli2.closest(source_word, word_list) == "apple"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_find_closest_token_different_lengths():
|
|
37
|
+
source_word = "longword"
|
|
38
|
+
word_list = ["short", "longer", "longestword"]
|
|
39
|
+
assert cli2.closest(source_word, word_list) == "longestword"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_find_closest_token_same_distance_multiple():
|
|
43
|
+
source_word = "test"
|
|
44
|
+
word_list = ["tesa", "tesb", "tesc"]
|
|
45
|
+
# difflib will pick the first if same distances.
|
|
46
|
+
assert cli2.closest(source_word, word_list) == "tesa"
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|