galactic 0.4.0.0.post1.dev132__py3-none-any.whl
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.
- galactic/apps/cli/main/__init__.py +27 -0
- galactic/apps/cli/main/__init__.pyi +22 -0
- galactic/apps/cli/main/_app.py +335 -0
- galactic/apps/cli/main/_pip_config.py +516 -0
- galactic/apps/cli/main/py.typed +0 -0
- galactic-0.4.0.0.post1.dev132.dist-info/METADATA +174 -0
- galactic-0.4.0.0.post1.dev132.dist-info/RECORD +10 -0
- galactic-0.4.0.0.post1.dev132.dist-info/WHEEL +4 -0
- galactic-0.4.0.0.post1.dev132.dist-info/entry_points.txt +2 -0
- galactic-0.4.0.0.post1.dev132.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines the public API for the CLI application module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"application",
|
|
7
|
+
"error",
|
|
8
|
+
"info",
|
|
9
|
+
"warning",
|
|
10
|
+
"comment",
|
|
11
|
+
"prompt",
|
|
12
|
+
"confirm",
|
|
13
|
+
"AppStyles",
|
|
14
|
+
"Group",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ._app import (
|
|
18
|
+
AppStyles,
|
|
19
|
+
Group,
|
|
20
|
+
application,
|
|
21
|
+
comment,
|
|
22
|
+
confirm,
|
|
23
|
+
error,
|
|
24
|
+
info,
|
|
25
|
+
prompt,
|
|
26
|
+
warning,
|
|
27
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
import rich_click as click
|
|
4
|
+
|
|
5
|
+
class Group(click.Group): # type: ignore[misc]
|
|
6
|
+
...
|
|
7
|
+
|
|
8
|
+
application: Group
|
|
9
|
+
|
|
10
|
+
def error(text: str, **kwargs: Any) -> None: ...
|
|
11
|
+
def info(text: str, **kwargs: Any) -> None: ...
|
|
12
|
+
def warning(text: str, **kwargs: Any) -> None: ...
|
|
13
|
+
def comment(text: str, **kwargs: Any) -> None: ...
|
|
14
|
+
def prompt(text: str, **kwargs: Any) -> Any: ...
|
|
15
|
+
def confirm(text: str, **kwargs: Any) -> bool: ...
|
|
16
|
+
|
|
17
|
+
class AppStyles:
|
|
18
|
+
ERROR: ClassVar[dict[str, Any]]
|
|
19
|
+
WARNING: ClassVar[dict[str, Any]]
|
|
20
|
+
INFO: ClassVar[dict[str, Any]]
|
|
21
|
+
COMMENT: ClassVar[dict[str, Any]]
|
|
22
|
+
QUESTION: ClassVar[dict[str, Any]]
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main app module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
|
+
|
|
10
|
+
import rich_click as click
|
|
11
|
+
|
|
12
|
+
from galactic.helpers.core import detect_plugins
|
|
13
|
+
|
|
14
|
+
from ._pip_config import (
|
|
15
|
+
extract_url_username,
|
|
16
|
+
list_extra_index_urls,
|
|
17
|
+
redact_url_credentials,
|
|
18
|
+
resolve_pip_config_path,
|
|
19
|
+
write_pip_extra_index_url,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# pylint: disable=too-few-public-methods
|
|
24
|
+
class AppStyles:
|
|
25
|
+
"""
|
|
26
|
+
Styles used in application.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
ERROR
|
|
31
|
+
Style for errors (``{"fg": "red", "bold": True}`` by default).
|
|
32
|
+
WARNING
|
|
33
|
+
Style for warnings (``{"fg": "yellow", "bold": True}`` by default).
|
|
34
|
+
INFO
|
|
35
|
+
Style for informations (``{"fg": "bright_magenta"}`` by default).
|
|
36
|
+
COMMENT
|
|
37
|
+
Style for comments (``{"fg": "green"}`` by default).
|
|
38
|
+
QUESTION
|
|
39
|
+
Style for questions (``{"fg": "cyan"}`` by default).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
ERROR: ClassVar[dict[str, Any]] = {"fg": "red", "bold": True}
|
|
43
|
+
WARNING: ClassVar[dict[str, Any]] = {"fg": "yellow", "bold": True}
|
|
44
|
+
INFO: ClassVar[dict[str, Any]] = {"fg": "bright_magenta"}
|
|
45
|
+
COMMENT: ClassVar[dict[str, Any]] = {"fg": "green"}
|
|
46
|
+
QUESTION: ClassVar[dict[str, Any]] = {"fg": "cyan"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def error(text: str, **kwargs: Any) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Display an error message.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
text
|
|
56
|
+
Text to display
|
|
57
|
+
**kwargs
|
|
58
|
+
Additional parameters passed to click.secho
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
click.secho(text, **AppStyles.ERROR, **kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def info(text: str, **kwargs: Any) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Display an info message.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
text
|
|
71
|
+
Text to display
|
|
72
|
+
**kwargs
|
|
73
|
+
Additional parameters passed to click.secho
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
click.secho(text, **AppStyles.INFO, **kwargs)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def warning(text: str, **kwargs: Any) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Display a warning.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
text
|
|
86
|
+
Text to display
|
|
87
|
+
**kwargs
|
|
88
|
+
Additional parameters passed to click.secho
|
|
89
|
+
|
|
90
|
+
"""
|
|
91
|
+
click.secho(text, **AppStyles.WARNING, **kwargs)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def comment(text: str, **kwargs: Any) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Display a comment.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
text
|
|
101
|
+
Text to display
|
|
102
|
+
**kwargs
|
|
103
|
+
Additional parameters passed to click.secho
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
click.secho(text, **AppStyles.COMMENT, **kwargs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def prompt(text: str, **kwargs: Any) -> Any:
|
|
110
|
+
"""
|
|
111
|
+
Prompt the user.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
text
|
|
116
|
+
Text to display
|
|
117
|
+
**kwargs
|
|
118
|
+
Additional parameters passed to click.prompt
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
return click.prompt(click.style(text, **AppStyles.QUESTION), **kwargs)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def confirm(text: str, **kwargs: Any) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Prompt the user for a confirmation.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
text
|
|
131
|
+
Text to display
|
|
132
|
+
**kwargs
|
|
133
|
+
Additional parameters passed to click.confirm
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
return click.confirm(click.style(text, **AppStyles.QUESTION), **kwargs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def prompt_password_with_confirmation(text: str) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Prompt a hidden password twice and allow empty values.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
text
|
|
146
|
+
Prompt displayed for the first password entry.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
str
|
|
151
|
+
Confirmed password.
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
while True:
|
|
155
|
+
password = prompt(
|
|
156
|
+
text,
|
|
157
|
+
default="",
|
|
158
|
+
hide_input=True,
|
|
159
|
+
show_default=False,
|
|
160
|
+
type=str,
|
|
161
|
+
)
|
|
162
|
+
confirmation = prompt(
|
|
163
|
+
"Confirm password",
|
|
164
|
+
default="",
|
|
165
|
+
hide_input=True,
|
|
166
|
+
show_default=False,
|
|
167
|
+
type=str,
|
|
168
|
+
)
|
|
169
|
+
if password == confirmation:
|
|
170
|
+
return password
|
|
171
|
+
error("Passwords do not match. Please retry.")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# pylint: disable=too-few-public-methods
|
|
175
|
+
class AppEnvVars:
|
|
176
|
+
"""
|
|
177
|
+
Environment variables.
|
|
178
|
+
|
|
179
|
+
Attributes
|
|
180
|
+
----------
|
|
181
|
+
NO_COLOR
|
|
182
|
+
Name of environment variable for no-color.
|
|
183
|
+
FORCE_COLOR
|
|
184
|
+
Name of environment variable for force color.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
# https://no-color.org
|
|
188
|
+
NO_COLOR = "NO_COLOR"
|
|
189
|
+
FORCE_COLOR = "FORCE_COLOR"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Group(click.Group): # type: ignore[misc]
|
|
193
|
+
"""
|
|
194
|
+
Group class for the GALACTIC app.
|
|
195
|
+
|
|
196
|
+
It does two things:
|
|
197
|
+
|
|
198
|
+
* it detects plugins from the galactic.apps.cli.main group.
|
|
199
|
+
* it adds an epilog to all added commands.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
_plugins_detected = False
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def detect_plugins(cls) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Detect plugins from the galactic.apps.cli.main group.
|
|
208
|
+
"""
|
|
209
|
+
if not cls._plugins_detected:
|
|
210
|
+
detect_plugins(group="galactic.apps.cli.main")
|
|
211
|
+
cls._plugins_detected = True
|
|
212
|
+
|
|
213
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
214
|
+
"""
|
|
215
|
+
List commands in the group.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
ctx
|
|
220
|
+
The click context
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
list[str]
|
|
225
|
+
List of command names in the group
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
self.detect_plugins()
|
|
229
|
+
return super().list_commands(ctx)
|
|
230
|
+
|
|
231
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
232
|
+
"""
|
|
233
|
+
Get a command by name.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
ctx
|
|
238
|
+
The click context
|
|
239
|
+
cmd_name
|
|
240
|
+
The name of the command to get
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
click.Command | None
|
|
245
|
+
The command if found, otherwise None
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
self.detect_plugins()
|
|
249
|
+
return super().get_command(ctx, cmd_name)
|
|
250
|
+
|
|
251
|
+
def add_command(self, cmd: click.Command, name: str | None = None) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Add a command to the group.
|
|
254
|
+
|
|
255
|
+
This method also sets the epilog of the command to the application's epilog.
|
|
256
|
+
It is useful for commands that are part of the application and should
|
|
257
|
+
inherit the application's epilog.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
cmd
|
|
262
|
+
The command to add
|
|
263
|
+
name
|
|
264
|
+
The name of the command (if None, the name of the command is used)
|
|
265
|
+
|
|
266
|
+
"""
|
|
267
|
+
cmd.epilog = cmd.epilog or application.epilog
|
|
268
|
+
super().add_command(cmd, name)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@click.version_option(package_name="galactic") # type: ignore[untyped-decorator]
|
|
272
|
+
@click.group( # type: ignore[untyped-decorator]
|
|
273
|
+
name="galactic",
|
|
274
|
+
cls=Group,
|
|
275
|
+
epilog="GAlois LAttices, Concept Theory, Implicational systems and Closures.",
|
|
276
|
+
)
|
|
277
|
+
@click.pass_context # type: ignore[untyped-decorator]
|
|
278
|
+
def application(ctx: click.Context) -> int:
|
|
279
|
+
"""
|
|
280
|
+
GALACTIC main application.
|
|
281
|
+
"""
|
|
282
|
+
if os.environ.get(AppEnvVars.NO_COLOR) == "1":
|
|
283
|
+
ctx.color = False
|
|
284
|
+
elif os.environ.get(AppEnvVars.FORCE_COLOR) == "1":
|
|
285
|
+
ctx.color = True
|
|
286
|
+
return 0
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@application.command() # type: ignore[untyped-decorator]
|
|
290
|
+
def config() -> None:
|
|
291
|
+
"""
|
|
292
|
+
Configure access to the GALACTIC package index.
|
|
293
|
+
"""
|
|
294
|
+
config_path = resolve_pip_config_path()
|
|
295
|
+
backup_path = config_path.with_name(f"{config_path.name}.old")
|
|
296
|
+
had_existing_config = config_path.exists()
|
|
297
|
+
existing_urls = list_extra_index_urls(config_path)
|
|
298
|
+
|
|
299
|
+
selected_url: str | None = None
|
|
300
|
+
if len(existing_urls) > 1:
|
|
301
|
+
info("Several extra-index-url entries were found:")
|
|
302
|
+
for idx, url in enumerate(existing_urls, start=1):
|
|
303
|
+
comment(f"{idx}. {redact_url_credentials(url)}")
|
|
304
|
+
selected_index = prompt(
|
|
305
|
+
"Which index URL do you want to configure?",
|
|
306
|
+
default=1,
|
|
307
|
+
type=click.IntRange(min=1, max=len(existing_urls)),
|
|
308
|
+
show_default=True,
|
|
309
|
+
)
|
|
310
|
+
selected_url = existing_urls[selected_index - 1]
|
|
311
|
+
elif len(existing_urls) == 1:
|
|
312
|
+
selected_url = existing_urls[0]
|
|
313
|
+
|
|
314
|
+
if selected_url is not None and not confirm(
|
|
315
|
+
"Index URL already present in "
|
|
316
|
+
f"{config_path}: {redact_url_credentials(selected_url)}. "
|
|
317
|
+
"Update credentials?",
|
|
318
|
+
default=False,
|
|
319
|
+
):
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
default_username = extract_url_username(selected_url) if selected_url else None
|
|
323
|
+
username = prompt(
|
|
324
|
+
"Username",
|
|
325
|
+
default=default_username or "__token__",
|
|
326
|
+
show_default=True,
|
|
327
|
+
type=str,
|
|
328
|
+
)
|
|
329
|
+
password = prompt_password_with_confirmation(
|
|
330
|
+
"Password (hit enter if you have no password)",
|
|
331
|
+
)
|
|
332
|
+
write_pip_extra_index_url(username, password, config_path, selected_url)
|
|
333
|
+
if had_existing_config:
|
|
334
|
+
warning(f"Previous configuration stored in {backup_path}.")
|
|
335
|
+
info(f"Stored credentials in {config_path}.")
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for pip configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from configparser import ConfigParser
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
from urllib.parse import quote, unquote, urlsplit, urlunsplit
|
|
15
|
+
|
|
16
|
+
import platformdirs
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
PIP_CONFIG_FILE_ENV_VAR = "PIP_CONFIG_FILE"
|
|
23
|
+
VIRTUAL_ENV_ENV_VAR = "VIRTUAL_ENV"
|
|
24
|
+
CONDA_PREFIX_ENV_VAR = "CONDA_PREFIX"
|
|
25
|
+
WINDOWS_PIP_CONFIG_FILENAME = "pip.ini"
|
|
26
|
+
UNIX_PIP_CONFIG_FILENAME = "pip.conf"
|
|
27
|
+
INDEX_HOSTNAME = "www.thegalactic.org"
|
|
28
|
+
PACKAGE_INDEX_URL = "https://www.thegalactic.org/pypi/simple/"
|
|
29
|
+
GLOBAL_SECTION = "global"
|
|
30
|
+
EXTRA_INDEX_URL_OPTION = "extra-index-url"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def pip_config_filename() -> str:
|
|
34
|
+
"""
|
|
35
|
+
Return the pip configuration filename for the current platform.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
str
|
|
40
|
+
``pip.ini`` on Windows, ``pip.conf`` elsewhere.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
if sys.platform.startswith("win"):
|
|
44
|
+
return WINDOWS_PIP_CONFIG_FILENAME
|
|
45
|
+
return UNIX_PIP_CONFIG_FILENAME
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_pip_config_path(
|
|
49
|
+
*,
|
|
50
|
+
env: Mapping[str, str] | None = None,
|
|
51
|
+
) -> Path:
|
|
52
|
+
"""
|
|
53
|
+
Resolve the pip configuration file path.
|
|
54
|
+
|
|
55
|
+
The resolution follows this priority order:
|
|
56
|
+
|
|
57
|
+
1. ``PIP_CONFIG_FILE`` environment variable (explicit override).
|
|
58
|
+
3. ``CONDA_PREFIX`` — path inside the active conda environment.
|
|
59
|
+
2. ``VIRTUAL_ENV`` — path inside the active virtual environment.
|
|
60
|
+
4. User-level config directory resolved by :mod:`platformdirs`.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
env
|
|
65
|
+
Environment variables mapping. Defaults to :data:`os.environ`.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
Path
|
|
70
|
+
The path to the pip configuration file.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
environment: Mapping[str, str] = env if env is not None else os.environ
|
|
74
|
+
filename = pip_config_filename()
|
|
75
|
+
|
|
76
|
+
# Priority 1: explicit override
|
|
77
|
+
if pip_config_file := environment.get(PIP_CONFIG_FILE_ENV_VAR):
|
|
78
|
+
return Path(pip_config_file)
|
|
79
|
+
|
|
80
|
+
# Priority 2: inside a conda environment
|
|
81
|
+
if conda_prefix := environment.get(CONDA_PREFIX_ENV_VAR):
|
|
82
|
+
return Path(conda_prefix) / filename
|
|
83
|
+
|
|
84
|
+
# Priority 3: inside a virtual environment
|
|
85
|
+
if virtual_env := environment.get(VIRTUAL_ENV_ENV_VAR):
|
|
86
|
+
return Path(virtual_env) / filename
|
|
87
|
+
|
|
88
|
+
# Priority 4: user-level config resolved by platformdirs
|
|
89
|
+
return platformdirs.user_config_path("pip") / filename
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_package_index_url(password: str, username: str = "__token__") -> str:
|
|
93
|
+
"""
|
|
94
|
+
Build the authenticated GALACTIC package index URL.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
password
|
|
99
|
+
Password to embed in the URL.
|
|
100
|
+
username
|
|
101
|
+
Username to embed in the URL (``__token__`` by default).
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
str
|
|
106
|
+
The package index URL with credentials embedded.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
return with_url_credentials(PACKAGE_INDEX_URL, username=username, password=password)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def list_extra_index_urls(path: Path) -> list[str]:
|
|
113
|
+
"""
|
|
114
|
+
Return configured ``extra-index-url`` entries from a pip config file.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
path
|
|
119
|
+
Path to the pip configuration file.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
list[str]
|
|
124
|
+
Parsed URL entries, in file order.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
if not path.exists():
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
config = ConfigParser(interpolation=None)
|
|
131
|
+
config.read(path, encoding="utf-8")
|
|
132
|
+
existing_raw = config.get(GLOBAL_SECTION, EXTRA_INDEX_URL_OPTION, fallback="")
|
|
133
|
+
return [url for raw in existing_raw.splitlines() for url in raw.split() if url]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def with_url_credentials(url: str, *, username: str, password: str) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Return ``url`` with explicit credentials in its netloc.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
url
|
|
143
|
+
Base URL to update.
|
|
144
|
+
username
|
|
145
|
+
Username to inject.
|
|
146
|
+
password
|
|
147
|
+
Password to inject.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
str
|
|
152
|
+
URL containing ``username:password`` credentials.
|
|
153
|
+
|
|
154
|
+
"""
|
|
155
|
+
parsed = urlsplit(url)
|
|
156
|
+
if not parsed.scheme or not parsed.hostname:
|
|
157
|
+
return url
|
|
158
|
+
|
|
159
|
+
user = quote(username, safe="")
|
|
160
|
+
secret = quote(password, safe="")
|
|
161
|
+
|
|
162
|
+
host = _as_text(parsed.hostname)
|
|
163
|
+
if parsed.port is not None:
|
|
164
|
+
host = f"{host}:{parsed.port}"
|
|
165
|
+
if user:
|
|
166
|
+
host = f"{user}:{secret}@{host}"
|
|
167
|
+
|
|
168
|
+
scheme = _as_text(parsed.scheme)
|
|
169
|
+
path = _as_text(parsed.path)
|
|
170
|
+
query = _as_text(parsed.query)
|
|
171
|
+
fragment = _as_text(parsed.fragment)
|
|
172
|
+
|
|
173
|
+
return urlunsplit((scheme, host, path, query, fragment))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def redact_url_credentials(url: str) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Return ``url`` without embedded credentials.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
url
|
|
183
|
+
URL potentially containing ``username:password``.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
str
|
|
188
|
+
URL with user-info stripped from netloc.
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
parsed = urlsplit(url)
|
|
192
|
+
if not parsed.scheme or not parsed.hostname:
|
|
193
|
+
return url
|
|
194
|
+
|
|
195
|
+
host = _as_text(parsed.hostname)
|
|
196
|
+
if parsed.port is not None:
|
|
197
|
+
host = f"{host}:{parsed.port}"
|
|
198
|
+
|
|
199
|
+
scheme = _as_text(parsed.scheme)
|
|
200
|
+
path = _as_text(parsed.path)
|
|
201
|
+
query = _as_text(parsed.query)
|
|
202
|
+
fragment = _as_text(parsed.fragment)
|
|
203
|
+
return urlunsplit((scheme, host, path, query, fragment))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def extract_url_username(url: str) -> str | None:
|
|
207
|
+
"""
|
|
208
|
+
Extract the username embedded in ``url`` credentials.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
url
|
|
213
|
+
URL that may contain user-info.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
str | None
|
|
218
|
+
Decoded username when present, otherwise ``None``.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
parsed = urlsplit(url)
|
|
222
|
+
if parsed.username is None:
|
|
223
|
+
return None
|
|
224
|
+
username = unquote(_as_text(parsed.username))
|
|
225
|
+
return username or None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_galactic_url_configured(path: Path) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Check whether a GALACTIC index URL is already present in a pip config file.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
path
|
|
235
|
+
Path to the pip configuration file.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
bool
|
|
240
|
+
``True`` if at least one ``extra-index-url`` entry references
|
|
241
|
+
:data:`INDEX_HOSTNAME`, ``False`` otherwise (including when the file
|
|
242
|
+
does not exist).
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
return any(INDEX_HOSTNAME in url for url in list_extra_index_urls(path))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def write_pip_extra_index_url(
|
|
249
|
+
username: str,
|
|
250
|
+
password: str,
|
|
251
|
+
path: Path,
|
|
252
|
+
target_url: str | None = None,
|
|
253
|
+
) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Write the GALACTIC extra index URL to a pip configuration file.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
username
|
|
260
|
+
Username to store in the authenticated URL.
|
|
261
|
+
password
|
|
262
|
+
Password to store in the authenticated URL.
|
|
263
|
+
path
|
|
264
|
+
Path to the pip configuration file.
|
|
265
|
+
target_url
|
|
266
|
+
Existing URL to update. If ``None``, use the GALACTIC URL when no
|
|
267
|
+
``extra-index-url`` is configured.
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
str
|
|
272
|
+
Final URL that has been configured with credentials.
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
all_urls = list_extra_index_urls(path)
|
|
276
|
+
|
|
277
|
+
selected_url = target_url
|
|
278
|
+
if selected_url is None and not all_urls:
|
|
279
|
+
selected_url = PACKAGE_INDEX_URL
|
|
280
|
+
elif selected_url is None and len(all_urls) == 1:
|
|
281
|
+
selected_url = all_urls[0]
|
|
282
|
+
|
|
283
|
+
if selected_url is None:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
"target_url must be provided when multiple URLs are configured",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
configured_url = with_url_credentials(
|
|
289
|
+
selected_url,
|
|
290
|
+
username=username,
|
|
291
|
+
password=password,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
updated = False
|
|
295
|
+
for index, url in enumerate(all_urls):
|
|
296
|
+
if url == selected_url:
|
|
297
|
+
all_urls[index] = configured_url
|
|
298
|
+
updated = True
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
if not updated:
|
|
302
|
+
all_urls.append(configured_url)
|
|
303
|
+
|
|
304
|
+
new_content = _upsert_extra_index_url_option(
|
|
305
|
+
original_content=path.read_text(encoding="utf-8") if path.exists() else "",
|
|
306
|
+
urls=all_urls,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
if path.exists():
|
|
311
|
+
backup_path = path.with_name(f"{path.name}.old")
|
|
312
|
+
shutil.copy2(path, backup_path)
|
|
313
|
+
path.write_text(new_content, encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
return configured_url
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# pylint: disable=too-many-locals, too-many-branches
|
|
319
|
+
def _upsert_extra_index_url_option(*, original_content: str, urls: list[str]) -> str:
|
|
320
|
+
"""Update only `[global].extra-index-url` while preserving surrounding text."""
|
|
321
|
+
newline = _detect_newline(original_content)
|
|
322
|
+
section_re = re.compile(r"^\s*\[(?P<name>[^\]]+)\]\s*$", re.IGNORECASE)
|
|
323
|
+
option_re = re.compile(r"^(?P<indent>\s*)extra-index-url\s*=.*$", re.IGNORECASE)
|
|
324
|
+
|
|
325
|
+
lines = original_content.splitlines(keepends=True)
|
|
326
|
+
section_start: int | None = None
|
|
327
|
+
section_end = len(lines)
|
|
328
|
+
|
|
329
|
+
for index, line in enumerate(lines):
|
|
330
|
+
match = section_re.match(line.rstrip("\r\n"))
|
|
331
|
+
if match and match.group("name").strip().lower() == GLOBAL_SECTION:
|
|
332
|
+
section_start = index
|
|
333
|
+
for after in range(index + 1, len(lines)):
|
|
334
|
+
if section_re.match(lines[after].rstrip("\r\n")):
|
|
335
|
+
section_end = after
|
|
336
|
+
break
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
replacement = _render_extra_index_option_lines(
|
|
340
|
+
urls,
|
|
341
|
+
newline=newline,
|
|
342
|
+
indent="",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if section_start is None:
|
|
346
|
+
if lines and not lines[-1].endswith(("\n", "\r")):
|
|
347
|
+
lines[-1] = lines[-1] + newline
|
|
348
|
+
if lines and lines[-1].strip():
|
|
349
|
+
lines.append(newline)
|
|
350
|
+
lines.append(f"[{GLOBAL_SECTION}]{newline}")
|
|
351
|
+
lines.extend(replacement)
|
|
352
|
+
return "".join(lines)
|
|
353
|
+
|
|
354
|
+
option_start: int | None = None
|
|
355
|
+
option_end = section_end
|
|
356
|
+
option_indent = ""
|
|
357
|
+
for index in range(section_start + 1, section_end):
|
|
358
|
+
stripped = lines[index].rstrip("\r\n")
|
|
359
|
+
match = option_re.match(stripped)
|
|
360
|
+
if match:
|
|
361
|
+
option_start = index
|
|
362
|
+
option_indent = match.group("indent")
|
|
363
|
+
for after in range(index + 1, section_end):
|
|
364
|
+
candidate = lines[after].rstrip("\r\n")
|
|
365
|
+
if not candidate:
|
|
366
|
+
continue
|
|
367
|
+
if candidate.startswith((" ", "\t")):
|
|
368
|
+
continue
|
|
369
|
+
if section_re.match(candidate):
|
|
370
|
+
break
|
|
371
|
+
option_end = after
|
|
372
|
+
break
|
|
373
|
+
else:
|
|
374
|
+
option_end = section_end
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
if option_start is None:
|
|
378
|
+
insertion_at = section_end
|
|
379
|
+
if insertion_at > section_start + 1 and lines[insertion_at - 1].strip():
|
|
380
|
+
lines.insert(insertion_at, newline)
|
|
381
|
+
insertion_at += 1
|
|
382
|
+
lines[insertion_at:insertion_at] = _render_extra_index_option_lines(
|
|
383
|
+
urls,
|
|
384
|
+
newline=newline,
|
|
385
|
+
indent="",
|
|
386
|
+
)
|
|
387
|
+
return "".join(lines)
|
|
388
|
+
|
|
389
|
+
lines[option_start:option_end] = _rewrite_option_block_preserving_positions(
|
|
390
|
+
lines[option_start:option_end],
|
|
391
|
+
urls,
|
|
392
|
+
newline=newline,
|
|
393
|
+
option_indent=option_indent,
|
|
394
|
+
)
|
|
395
|
+
return "".join(lines)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _render_extra_index_option_lines(
|
|
399
|
+
urls: list[str],
|
|
400
|
+
*,
|
|
401
|
+
newline: str,
|
|
402
|
+
indent: str,
|
|
403
|
+
) -> list[str]:
|
|
404
|
+
if len(urls) == 1:
|
|
405
|
+
return [f"{indent}{EXTRA_INDEX_URL_OPTION} = {urls[0]}{newline}"]
|
|
406
|
+
rendered = [f"{indent}{EXTRA_INDEX_URL_OPTION} ={newline}"]
|
|
407
|
+
rendered.extend(f"{indent} {url}{newline}" for url in urls)
|
|
408
|
+
return rendered
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _rewrite_option_block_preserving_positions(
|
|
412
|
+
original_option_lines: list[str],
|
|
413
|
+
urls: list[str],
|
|
414
|
+
*,
|
|
415
|
+
newline: str,
|
|
416
|
+
option_indent: str,
|
|
417
|
+
) -> list[str]:
|
|
418
|
+
"""Rewrite URL lines in an option block while keeping other lines unchanged."""
|
|
419
|
+
if not original_option_lines:
|
|
420
|
+
return _render_extra_index_option_lines(
|
|
421
|
+
urls,
|
|
422
|
+
newline=newline,
|
|
423
|
+
indent=option_indent,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
url_slots: list[tuple[str, str]] = []
|
|
427
|
+
passthrough: dict[int, str] = {}
|
|
428
|
+
|
|
429
|
+
first = original_option_lines[0].rstrip("\r\n")
|
|
430
|
+
first_match = re.match(
|
|
431
|
+
r"^(?P<prefix>\s*extra-index-url\s*=\s*)(?P<value>.*)$",
|
|
432
|
+
first,
|
|
433
|
+
re.IGNORECASE,
|
|
434
|
+
)
|
|
435
|
+
if first_match is None:
|
|
436
|
+
return _render_extra_index_option_lines(
|
|
437
|
+
urls,
|
|
438
|
+
newline=newline,
|
|
439
|
+
indent=option_indent,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
first_value = first_match.group("value").strip()
|
|
443
|
+
first_prefix = first_match.group("prefix")
|
|
444
|
+
if _is_url_value_line(first_value):
|
|
445
|
+
url_slots.append((first_prefix, "first"))
|
|
446
|
+
else:
|
|
447
|
+
passthrough[0] = (
|
|
448
|
+
f"{first_prefix}{newline}" if not first_value else f"{first}{newline}"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
for index, raw_line in enumerate(original_option_lines[1:], start=1):
|
|
452
|
+
line = raw_line.rstrip("\r\n")
|
|
453
|
+
stripped = line.strip()
|
|
454
|
+
if not stripped or stripped.startswith(("#", ";")):
|
|
455
|
+
passthrough[index] = f"{line}{newline}"
|
|
456
|
+
continue
|
|
457
|
+
indent_match = re.match(r"^(\s*)", line)
|
|
458
|
+
slot_prefix = indent_match.group(1) if indent_match else ""
|
|
459
|
+
url_slots.append((slot_prefix, "cont"))
|
|
460
|
+
|
|
461
|
+
result: list[str] = []
|
|
462
|
+
url_index = 0
|
|
463
|
+
total_lines = len(original_option_lines)
|
|
464
|
+
slot_index = 0
|
|
465
|
+
|
|
466
|
+
for index in range(total_lines):
|
|
467
|
+
if index in passthrough:
|
|
468
|
+
result.append(passthrough[index])
|
|
469
|
+
continue
|
|
470
|
+
if url_index >= len(urls):
|
|
471
|
+
slot_index += 1
|
|
472
|
+
continue
|
|
473
|
+
prefix = url_slots[slot_index][0]
|
|
474
|
+
result.append(f"{prefix}{urls[url_index]}{newline}")
|
|
475
|
+
url_index += 1
|
|
476
|
+
slot_index += 1
|
|
477
|
+
|
|
478
|
+
while url_index < len(urls):
|
|
479
|
+
if url_slots:
|
|
480
|
+
base_prefix = url_slots[-1][0]
|
|
481
|
+
continuation_prefix = (
|
|
482
|
+
base_prefix if base_prefix.strip() else f"{option_indent} "
|
|
483
|
+
)
|
|
484
|
+
else:
|
|
485
|
+
continuation_prefix = f"{option_indent} "
|
|
486
|
+
result.append(f"{continuation_prefix}{urls[url_index]}{newline}")
|
|
487
|
+
url_index += 1
|
|
488
|
+
|
|
489
|
+
if result and not result[0].lower().lstrip().startswith("extra-index-url"):
|
|
490
|
+
return _render_extra_index_option_lines(
|
|
491
|
+
urls,
|
|
492
|
+
newline=newline,
|
|
493
|
+
indent=option_indent,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _is_url_value_line(value: str) -> bool:
|
|
500
|
+
if not value:
|
|
501
|
+
return False
|
|
502
|
+
return not value.startswith(("#", ";"))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _detect_newline(content: str) -> str:
|
|
506
|
+
if "\r\n" in content:
|
|
507
|
+
return "\r\n"
|
|
508
|
+
return "\n"
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _as_text(value: str | bytes | None) -> str:
|
|
512
|
+
if value is None:
|
|
513
|
+
return ""
|
|
514
|
+
if isinstance(value, bytes):
|
|
515
|
+
return value.decode()
|
|
516
|
+
return value
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: galactic
|
|
3
|
+
Version: 0.4.0.0.post1.dev132
|
|
4
|
+
Summary: GALACTIC main application
|
|
5
|
+
Project-URL: Homepage, https://www.thegalactic.org/
|
|
6
|
+
Project-URL: Documentation, https://www.thegalactic.org/docs/apps/cli/main/
|
|
7
|
+
Project-URL: Repository, https://gitlab.univ-lr.fr/galactic/public/src/apps/cli/galactic-app-cli-main
|
|
8
|
+
Project-URL: Issues, https://gitlab.univ-lr.fr/galactic/public/src/apps/cli/galactic-app-cli-main/-/issues
|
|
9
|
+
Project-URL: Icon, https://www.thegalactic.org/docs/apps/cli/main/latest/_static/icon.svg
|
|
10
|
+
Project-URL: Icon-dark, https://www.thegalactic.org/docs/apps/cli/main/latest/_static/icon-dark.svg
|
|
11
|
+
Project-URL: Index-public, https://www.thegalactic.org/pypi/public/simple/
|
|
12
|
+
Author-email: The Galactic Organization <contact@thegalactic.org>
|
|
13
|
+
Maintainer-email: The Galactic Organization <contact@thegalactic.org>
|
|
14
|
+
License-Expression: BSD-3-Clause
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Keywords: application,formal concept analysis
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Classifier: Environment :: Console
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
21
|
+
Classifier: Natural Language :: English
|
|
22
|
+
Classifier: Operating System :: OS Independent
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Requires-Python: <3.15,>=3.11
|
|
28
|
+
Requires-Dist: click~=8.3
|
|
29
|
+
Requires-Dist: galactic-helper-core>=0.4.0.0.post1.dev
|
|
30
|
+
Requires-Dist: platformdirs~=4.9
|
|
31
|
+
Requires-Dist: rich-click~=1.9
|
|
32
|
+
Description-Content-Type: text/x-rst
|
|
33
|
+
|
|
34
|
+
Instructions
|
|
35
|
+
============
|
|
36
|
+
|
|
37
|
+
|hatch|
|
|
38
|
+
|pre-commit|
|
|
39
|
+
|ruff|
|
|
40
|
+
|black|
|
|
41
|
+
|doc8|
|
|
42
|
+
|mypy|
|
|
43
|
+
|pylint|
|
|
44
|
+
|slotscheck|
|
|
45
|
+
|
|
46
|
+
.. |hatch| image:: https://img.shields.io/badge/%F0%9F%A5%9A-hatch-4051b5.svg
|
|
47
|
+
:target: https://pypi.org/project/hatch/
|
|
48
|
+
.. |pre-commit| image:: https://img.shields.io/badge/-pre--commit-brightgreen?logo=pre-commit&labelColor=gray
|
|
49
|
+
:target: https://pypi.org/project/pre-commit/
|
|
50
|
+
.. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
|
|
51
|
+
:target: https://pypi.org/project/ruff/
|
|
52
|
+
.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
53
|
+
:target: https://pypi.org/project/black/
|
|
54
|
+
.. |doc8| image:: https://img.shields.io/badge/static--checked-doc8-green
|
|
55
|
+
:target: https://pypi.org/project/doc8/
|
|
56
|
+
.. |mypy| image:: https://img.shields.io/badge/static--checked-mypy-blue
|
|
57
|
+
:target: https://pypi.org/project/mypy/
|
|
58
|
+
.. |pylint| image:: https://img.shields.io/badge/dynamic--checked-pylint-orange
|
|
59
|
+
:target: https://pypi.org/project/pylint/
|
|
60
|
+
.. |slotscheck| image:: https://img.shields.io/badge/dynamic--checked-slotscheck-orange
|
|
61
|
+
:target: https://pypi.org/project/slotscheck/
|
|
62
|
+
|
|
63
|
+
Prerequisite
|
|
64
|
+
------------
|
|
65
|
+
|
|
66
|
+
*galactic* requires `python 3.11`_,
|
|
67
|
+
a programming language that comes pre-installed on linux and Mac OS X,
|
|
68
|
+
and which is easily installed `on Windows`_;
|
|
69
|
+
|
|
70
|
+
Installation
|
|
71
|
+
------------
|
|
72
|
+
|
|
73
|
+
Install *galactic* in a virtual (``conda`` or ``venv``)
|
|
74
|
+
environment using the bash commands:
|
|
75
|
+
|
|
76
|
+
.. code-block:: shell-session
|
|
77
|
+
|
|
78
|
+
(galactic) $ pip install galactic
|
|
79
|
+
|
|
80
|
+
Don't forget to add the ``--pre`` flag if you want the latest unstable build.
|
|
81
|
+
|
|
82
|
+
After installation, you can configure authenticated access to the
|
|
83
|
+
**GALACTIC** package index with:
|
|
84
|
+
|
|
85
|
+
.. code-block:: shell-session
|
|
86
|
+
|
|
87
|
+
(galactic) $ galactic config
|
|
88
|
+
|
|
89
|
+
This command prompts for your GALACTIC password and stores it in your
|
|
90
|
+
``pip.conf`` or ``pip.ini`` file.
|
|
91
|
+
|
|
92
|
+
Contributing
|
|
93
|
+
------------
|
|
94
|
+
|
|
95
|
+
Build
|
|
96
|
+
~~~~~
|
|
97
|
+
|
|
98
|
+
Building *galactic* requires
|
|
99
|
+
|
|
100
|
+
* `hatch`_, which is a tool for dependency management and packaging in Python;
|
|
101
|
+
|
|
102
|
+
Build *galactic* using the bash command
|
|
103
|
+
|
|
104
|
+
.. code-block:: shell-session
|
|
105
|
+
|
|
106
|
+
$ hatch build
|
|
107
|
+
|
|
108
|
+
Testing
|
|
109
|
+
~~~~~~~
|
|
110
|
+
|
|
111
|
+
Test *galactic* using the bash command:
|
|
112
|
+
|
|
113
|
+
.. code-block:: shell-session
|
|
114
|
+
|
|
115
|
+
$ hatch test
|
|
116
|
+
|
|
117
|
+
for running the tests.
|
|
118
|
+
|
|
119
|
+
.. code-block:: shell-session
|
|
120
|
+
|
|
121
|
+
$ hatch test --cover
|
|
122
|
+
|
|
123
|
+
for running the tests with the coverage.
|
|
124
|
+
|
|
125
|
+
.. code-block:: shell-session
|
|
126
|
+
|
|
127
|
+
$ hatch test --doctest-modules src
|
|
128
|
+
|
|
129
|
+
for running the `doctest`.
|
|
130
|
+
|
|
131
|
+
Linting
|
|
132
|
+
~~~~~~~
|
|
133
|
+
|
|
134
|
+
Lint *galactic* using the bash commands:
|
|
135
|
+
|
|
136
|
+
.. code-block:: shell-session
|
|
137
|
+
|
|
138
|
+
$ hatch fmt --check
|
|
139
|
+
|
|
140
|
+
for running static linting.
|
|
141
|
+
|
|
142
|
+
.. code-block:: shell-session
|
|
143
|
+
|
|
144
|
+
$ hatch fmt
|
|
145
|
+
|
|
146
|
+
for automatic fixing of static linting issues.
|
|
147
|
+
|
|
148
|
+
.. code-block:: shell-session
|
|
149
|
+
|
|
150
|
+
$ hatch run lint:check
|
|
151
|
+
|
|
152
|
+
for running dynamic linting.
|
|
153
|
+
|
|
154
|
+
Documentation
|
|
155
|
+
~~~~~~~~~~~~~
|
|
156
|
+
|
|
157
|
+
Build the documentation using the bash commands:
|
|
158
|
+
|
|
159
|
+
.. code-block:: shell-session
|
|
160
|
+
|
|
161
|
+
$ hatch run docs:build
|
|
162
|
+
|
|
163
|
+
Getting Help
|
|
164
|
+
~~~~~~~~~~~~
|
|
165
|
+
|
|
166
|
+
.. important::
|
|
167
|
+
|
|
168
|
+
If you have any difficulties with *galactic*, please feel welcome to
|
|
169
|
+
`file an issue`_ on GitLab so that we can help.
|
|
170
|
+
|
|
171
|
+
.. _file an issue: https://gitlab.univ-lr.fr/galactic/public/src/apps/cli/galactic-app-cli-main/-/issues
|
|
172
|
+
.. _python 3.11: http://www.python.org
|
|
173
|
+
.. _on Windows: https://www.python.org/downloads/windows
|
|
174
|
+
.. _hatch: https://hatch.pypa.io/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
galactic/apps/cli/main/__init__.py,sha256=WTAPb5xi8j38e0Ol5zAs4l42BFwQO3qWi9JIbDI1Hsc,349
|
|
2
|
+
galactic/apps/cli/main/__init__.pyi,sha256=4IoZmUoosDWFDRh5mjLKH8F5YkaOmLQ21MDFGaX-1TY,642
|
|
3
|
+
galactic/apps/cli/main/_app.py,sha256=KXU8e-Rc2t96QtdlftQrB4_tCZG7YYenpeKo4h6uD6Q,8361
|
|
4
|
+
galactic/apps/cli/main/_pip_config.py,sha256=lFYQoAH6HL2VJBhCoTJz_BwSycX4rND7jat2egKlDQQ,14350
|
|
5
|
+
galactic/apps/cli/main/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
galactic-0.4.0.0.post1.dev132.dist-info/METADATA,sha256=Vs3Opf43t1uIH4LQNRwNl2jVfMf7-5tV1ixEvVoOIyM,4950
|
|
7
|
+
galactic-0.4.0.0.post1.dev132.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
galactic-0.4.0.0.post1.dev132.dist-info/entry_points.txt,sha256=xiTJDeF4B8NgXGgNE4bKmedDDGxf23Puwmfm1r3-N3o,64
|
|
9
|
+
galactic-0.4.0.0.post1.dev132.dist-info/licenses/LICENSE,sha256=9teLJoARXQ4o8xNJqYK8RmWCwTKvdLmdoHzqnStMAYE,1530
|
|
10
|
+
galactic-0.4.0.0.post1.dev132.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2026, The Galactic Organization
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|