pfund-kit 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.
- pfund_kit-0.1.0/PKG-INFO +24 -0
- pfund_kit-0.1.0/README.md +2 -0
- pfund_kit-0.1.0/pyproject.toml +38 -0
- pfund_kit-0.1.0/src/pfund_kit/__init__.py +5 -0
- pfund_kit-0.1.0/src/pfund_kit/aliase.py +283 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/__init__.py +4 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/commands/__init__.py +12 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/commands/config.py +239 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/commands/doc.py +65 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/commands/docker_compose.py +54 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/commands/remove.py +112 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/main.py +39 -0
- pfund_kit-0.1.0/src/pfund_kit/cli/utils.py +46 -0
- pfund_kit-0.1.0/src/pfund_kit/config.py +194 -0
- pfund_kit-0.1.0/src/pfund_kit/enums/notebook_type.py +7 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/__init__.py +162 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/configurator.py +105 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/filters/__init__.py +6 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/filters/trimmed_path_filter.py +67 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/formatters/__init__.py +6 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/formatters/colored_formatter.py +17 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/handlers/__init__.py +8 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/handlers/compressed_timed_rotating_file_handler.py +104 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/handlers/lazy_handler.py +155 -0
- pfund_kit-0.1.0/src/pfund_kit/logging/handlers/telegram_handler.py +28 -0
- pfund_kit-0.1.0/src/pfund_kit/paths.py +88 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/help.py +30 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/main.py +203 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/shell_group.py +64 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/toolbar.py +154 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/tutorial.py +44 -0
- pfund_kit-0.1.0/src/pfund_kit/pfund_shell/utils.py +140 -0
- pfund_kit-0.1.0/src/pfund_kit/style.py +291 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/__init__.py +238 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/function.py +51 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/progress_bar.py +286 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/singleton.py +22 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/temporal.py +71 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/text.py +26 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/toml.py +176 -0
- pfund_kit-0.1.0/src/pfund_kit/utils/yaml.py +171 -0
pfund_kit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pfund-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Configuration, CLI, and shared utilities for packages in pfund's ecosystem
|
|
5
|
+
Author: Stephen Yau
|
|
6
|
+
Author-email: Stephen Yau <softwareentrepreneer+pfund-kit@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Requires-Dist: rich>=14.2.0
|
|
9
|
+
Requires-Dist: tqdm>=4.67.1
|
|
10
|
+
Requires-Dist: click>=8.2.1
|
|
11
|
+
Requires-Dist: trogon>=0.6.0
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
13
|
+
Requires-Dist: tomlkit>=0.13.3
|
|
14
|
+
Requires-Dist: packaging>=25.0
|
|
15
|
+
Requires-Dist: platformdirs>=4.2.2
|
|
16
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
17
|
+
Requires-Dist: prompt-toolkit>=3.0.51
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Project-URL: homepage, https://pfund.ai
|
|
20
|
+
Project-URL: repository, https://github.com/PFund-Software-Ltd/pfund-kit
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pfund-kit
|
|
24
|
+
Configuration, CLI, and shared utilities for packages in pfund's ecosystem
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pfund-kit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Configuration, CLI, and shared utilities for packages in pfund's ecosystem"
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Stephen Yau", email = "softwareentrepreneer+pfund-kit@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"rich>=14.2.0",
|
|
13
|
+
"tqdm>=4.67.1",
|
|
14
|
+
"click>=8.2.1",
|
|
15
|
+
"trogon>=0.6.0",
|
|
16
|
+
"pyyaml>=6.0.3",
|
|
17
|
+
"tomlkit>=0.13.3",
|
|
18
|
+
"packaging>=25.0",
|
|
19
|
+
"platformdirs>=4.2.2",
|
|
20
|
+
"python-dotenv>=1.2.1",
|
|
21
|
+
"prompt-toolkit>=3.0.51",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
pfund-shell = "pfund_kit.pfund_shell.main:start_shell"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
homepage = "https://pfund.ai"
|
|
29
|
+
repository = "https://github.com/PFund-Software-Ltd/pfund-kit"
|
|
30
|
+
# documentation = ""
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv.build-backend]
|
|
37
|
+
module-name = "pfund_kit"
|
|
38
|
+
module-root = "src"
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# VIBE-CODED
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AliasRegistry:
|
|
8
|
+
"""
|
|
9
|
+
Thread-safe bidirectional alias registry for mapping short aliases to canonical names.
|
|
10
|
+
|
|
11
|
+
Features:
|
|
12
|
+
- Bidirectional lookup (alias ↔ canonical)
|
|
13
|
+
- Case-sensitive or case-insensitive matching
|
|
14
|
+
- Conflict detection during initialization
|
|
15
|
+
- Immutable after initialization
|
|
16
|
+
- Dict-like interface for easy integration
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
>>> registry = AliasRegistry({
|
|
20
|
+
... 'YF': 'YAHOO_FINANCE',
|
|
21
|
+
... 'FMP': 'FINANCIAL_MODELING_PREP',
|
|
22
|
+
... })
|
|
23
|
+
>>> # Forward: alias → canonical
|
|
24
|
+
>>> registry.resolve('YF')
|
|
25
|
+
'YAHOO_FINANCE'
|
|
26
|
+
>>> registry.resolve('YAHOO_FINANCE') # passthrough
|
|
27
|
+
'YAHOO_FINANCE'
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Reverse: canonical → alias (two ways)
|
|
30
|
+
>>> registry('YAHOO_FINANCE') # callable interface
|
|
31
|
+
'YF'
|
|
32
|
+
>>> registry.get_alias('YAHOO_FINANCE') # explicit method
|
|
33
|
+
'YF'
|
|
34
|
+
>>>
|
|
35
|
+
>>> # Membership testing
|
|
36
|
+
>>> 'YF' in registry
|
|
37
|
+
True
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
aliases: dict[str, str],
|
|
43
|
+
*,
|
|
44
|
+
case_sensitive: bool = True,
|
|
45
|
+
allow_conflicts: bool = False,
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the alias registry.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
aliases: Dictionary mapping alias → canonical name
|
|
52
|
+
case_sensitive: If False, all lookups are case-insensitive
|
|
53
|
+
allow_conflicts: If False, raises ValueError when an alias conflicts
|
|
54
|
+
with an existing canonical name
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If conflicts detected and allow_conflicts=False
|
|
58
|
+
"""
|
|
59
|
+
self._case_sensitive = case_sensitive
|
|
60
|
+
self._aliases: dict[str, str] = {}
|
|
61
|
+
self._reverse: dict[str, str] = {}
|
|
62
|
+
|
|
63
|
+
# Normalize keys if case-insensitive
|
|
64
|
+
self._normalize = (lambda x: x) if case_sensitive else (lambda x: x.lower())
|
|
65
|
+
|
|
66
|
+
# Validate and populate
|
|
67
|
+
for alias, canonical in aliases.items():
|
|
68
|
+
self._add_mapping(alias, canonical, allow_conflicts=allow_conflicts)
|
|
69
|
+
|
|
70
|
+
def _add_mapping(self, alias: str, canonical: str, allow_conflicts: bool = False) -> None:
|
|
71
|
+
"""Add a single alias → canonical mapping with conflict detection."""
|
|
72
|
+
norm_alias = self._normalize(alias)
|
|
73
|
+
norm_canonical = self._normalize(canonical)
|
|
74
|
+
|
|
75
|
+
# Check for conflicts: alias colliding with another canonical name
|
|
76
|
+
if not allow_conflicts and norm_alias in self._reverse:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Conflict: alias '{alias}' collides with existing canonical name"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Store normalized or original based on case sensitivity
|
|
82
|
+
key_alias = norm_alias if not self._case_sensitive else alias
|
|
83
|
+
key_canonical = norm_canonical if not self._case_sensitive else canonical
|
|
84
|
+
|
|
85
|
+
self._aliases[key_alias] = key_canonical
|
|
86
|
+
self._reverse[key_canonical] = key_alias
|
|
87
|
+
|
|
88
|
+
def resolve(self, name: str) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Resolve a name to its canonical form.
|
|
91
|
+
|
|
92
|
+
If the name is an alias, returns the canonical name.
|
|
93
|
+
If the name is already canonical (or unknown), returns it unchanged.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Alias or canonical name
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Canonical name
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
>>> registry.resolve('YF') # alias
|
|
103
|
+
'YAHOO_FINANCE'
|
|
104
|
+
>>> registry.resolve('YAHOO_FINANCE') # already canonical
|
|
105
|
+
'YAHOO_FINANCE'
|
|
106
|
+
>>> registry.resolve('UNKNOWN') # unknown, passthrough
|
|
107
|
+
'UNKNOWN'
|
|
108
|
+
"""
|
|
109
|
+
norm_name = self._normalize(name)
|
|
110
|
+
return self._aliases.get(norm_name, name)
|
|
111
|
+
|
|
112
|
+
def get_alias(self, canonical: str) -> str | None:
|
|
113
|
+
"""
|
|
114
|
+
Get the alias for a canonical name.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
canonical: Canonical name
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Alias if exists, None otherwise
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
>>> registry.get_alias('YAHOO_FINANCE')
|
|
124
|
+
'YF'
|
|
125
|
+
>>> registry.get_alias('UNKNOWN')
|
|
126
|
+
None
|
|
127
|
+
"""
|
|
128
|
+
norm_canonical = self._normalize(canonical)
|
|
129
|
+
return self._reverse.get(norm_canonical)
|
|
130
|
+
|
|
131
|
+
def __call__(self, canonical: str) -> str | None:
|
|
132
|
+
"""
|
|
133
|
+
Get the alias for a canonical name (callable interface).
|
|
134
|
+
|
|
135
|
+
This is a convenience method that calls get_alias() under the hood.
|
|
136
|
+
Provides a more intuitive API for reverse lookups.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
canonical: Canonical name
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Alias if exists, None otherwise
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
>>> registry = AliasRegistry({'YF': 'YAHOO_FINANCE'})
|
|
146
|
+
>>> registry('YAHOO_FINANCE')
|
|
147
|
+
'YF'
|
|
148
|
+
>>> registry('price') # if no alias exists
|
|
149
|
+
None
|
|
150
|
+
|
|
151
|
+
Note:
|
|
152
|
+
For forward lookups (alias → canonical), use resolve() instead.
|
|
153
|
+
"""
|
|
154
|
+
return self.get_alias(canonical)
|
|
155
|
+
|
|
156
|
+
def is_alias(self, name: str) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Check if a name is an alias (not a canonical name).
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
name: Name to check
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if name is an alias, False otherwise
|
|
165
|
+
"""
|
|
166
|
+
norm_name = self._normalize(name)
|
|
167
|
+
return norm_name in self._aliases
|
|
168
|
+
|
|
169
|
+
def is_canonical(self, name: str) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
Check if a name is a canonical name.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: Name to check
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True if name is a canonical name, False otherwise
|
|
178
|
+
"""
|
|
179
|
+
norm_name = self._normalize(name)
|
|
180
|
+
return norm_name in self._reverse
|
|
181
|
+
|
|
182
|
+
def __contains__(self, name: str) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Check if a name exists as either alias or canonical name.
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
>>> 'YF' in registry
|
|
188
|
+
True
|
|
189
|
+
>>> 'YAHOO_FINANCE' in registry
|
|
190
|
+
True
|
|
191
|
+
"""
|
|
192
|
+
norm_name = self._normalize(name)
|
|
193
|
+
return norm_name in self._aliases or norm_name in self._reverse
|
|
194
|
+
|
|
195
|
+
def __getitem__(self, alias: str) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Get canonical name for an alias (raises KeyError if not found).
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
>>> registry['YF']
|
|
201
|
+
'YAHOO_FINANCE'
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
KeyError: If alias not found
|
|
205
|
+
"""
|
|
206
|
+
norm_alias = self._normalize(alias)
|
|
207
|
+
return self._aliases[norm_alias]
|
|
208
|
+
|
|
209
|
+
def get(self, alias: str, default: str | None = None) -> str | None:
|
|
210
|
+
"""
|
|
211
|
+
Get canonical name for an alias with a default fallback.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
alias: Alias to look up
|
|
215
|
+
default: Default value if alias not found
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Canonical name or default
|
|
219
|
+
"""
|
|
220
|
+
norm_alias = self._normalize(alias)
|
|
221
|
+
return self._aliases.get(norm_alias, default)
|
|
222
|
+
|
|
223
|
+
def items(self) -> Iterator[tuple[str, str]]:
|
|
224
|
+
"""
|
|
225
|
+
Iterate over (alias, canonical) pairs.
|
|
226
|
+
|
|
227
|
+
Examples:
|
|
228
|
+
>>> for alias, canonical in registry.items():
|
|
229
|
+
... print(f"{alias} -> {canonical}")
|
|
230
|
+
"""
|
|
231
|
+
return iter(self._aliases.items())
|
|
232
|
+
|
|
233
|
+
def aliases(self) -> Iterator[str]:
|
|
234
|
+
"""
|
|
235
|
+
Get all aliases.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Iterator of alias names
|
|
239
|
+
"""
|
|
240
|
+
return iter(self._aliases.keys())
|
|
241
|
+
|
|
242
|
+
def canonicals(self) -> Iterator[str]:
|
|
243
|
+
"""
|
|
244
|
+
Get all canonical names.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Iterator of canonical names
|
|
248
|
+
"""
|
|
249
|
+
return iter(self._reverse.keys())
|
|
250
|
+
|
|
251
|
+
def to_dict(self) -> dict[str, str]:
|
|
252
|
+
"""
|
|
253
|
+
Export as a plain dictionary (alias → canonical).
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dictionary mapping aliases to canonical names
|
|
257
|
+
"""
|
|
258
|
+
return dict(self._aliases)
|
|
259
|
+
|
|
260
|
+
def to_reverse_dict(self) -> dict[str, str]:
|
|
261
|
+
"""
|
|
262
|
+
Export as a reverse dictionary (canonical → alias).
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Dictionary mapping canonical names to aliases
|
|
266
|
+
"""
|
|
267
|
+
return dict(self._reverse)
|
|
268
|
+
|
|
269
|
+
def __len__(self) -> int:
|
|
270
|
+
"""Return number of alias mappings."""
|
|
271
|
+
return len(self._aliases)
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
"""Return string representation."""
|
|
275
|
+
return f"AliasRegistry({self._aliases!r})"
|
|
276
|
+
|
|
277
|
+
def __str__(self) -> str:
|
|
278
|
+
"""Return human-readable string."""
|
|
279
|
+
items = [f" {a!r} -> {c!r}" for a, c in self._aliases.items()]
|
|
280
|
+
return f"AliasRegistry({len(self)} mappings):\n" + "\n".join(items)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = ['AliasRegistry']
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pfund_kit.cli.commands.config import config
|
|
2
|
+
from pfund_kit.cli.commands.docker_compose import docker_compose
|
|
3
|
+
from pfund_kit.cli.commands.remove import remove
|
|
4
|
+
from pfund_kit.cli.commands.doc import doc
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'config',
|
|
9
|
+
'docker_compose',
|
|
10
|
+
'remove',
|
|
11
|
+
'doc',
|
|
12
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def auto_detect_editor():
|
|
8
|
+
"""Auto-detect an available code editor from popular choices."""
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
# List of popular editors in order of preference
|
|
12
|
+
editors = ['cursor', 'code', 'zed', 'charm', 'nvim']
|
|
13
|
+
|
|
14
|
+
for cmd in editors:
|
|
15
|
+
if shutil.which(cmd):
|
|
16
|
+
return cmd
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def open_file_with_editor(file_path: Path, editor_cmd: str):
|
|
21
|
+
"""Open file with the specified editor, handling edge cases like Cursor's space bug."""
|
|
22
|
+
import subprocess
|
|
23
|
+
import platform
|
|
24
|
+
|
|
25
|
+
file_path_str = str(file_path)
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Cursor CLI has a bug with spaces in paths, use shell mode with quotes
|
|
29
|
+
if editor_cmd == 'cursor':
|
|
30
|
+
if platform.system() != 'Windows':
|
|
31
|
+
# On macOS/Linux: escape spaces with backslashes AND wrap in quotes
|
|
32
|
+
escaped_path = file_path_str.replace(' ', r'\ ')
|
|
33
|
+
subprocess.run(f'{editor_cmd} "{escaped_path}"', shell=True, check=True)
|
|
34
|
+
else:
|
|
35
|
+
# On Windows: use quotes (standard Windows shell escaping)
|
|
36
|
+
# Note: If this doesn't work, Windows may have the same bug
|
|
37
|
+
subprocess.run(f'{editor_cmd} "{file_path_str}"', shell=True, check=True)
|
|
38
|
+
else:
|
|
39
|
+
# All other editors: use standard subprocess list approach (safest)
|
|
40
|
+
subprocess.run([editor_cmd, file_path_str], check=True)
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
click.echo(f"Error: Editor '{editor_cmd}' not found. Please check if it's installed and in your PATH.", err=True)
|
|
43
|
+
raise
|
|
44
|
+
except subprocess.CalledProcessError as e:
|
|
45
|
+
click.echo(f"Error: Failed to open file with '{editor_cmd}': {e}", err=True)
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.group()
|
|
50
|
+
def config():
|
|
51
|
+
"""Manage configuration settings."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@config.command()
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def where(ctx):
|
|
58
|
+
"""Print the config path."""
|
|
59
|
+
config = ctx.obj['config']
|
|
60
|
+
click.echo(config.path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@config.command('list')
|
|
65
|
+
@click.pass_context
|
|
66
|
+
def list_config(ctx):
|
|
67
|
+
"""Print the config file."""
|
|
68
|
+
from pprint import pformat
|
|
69
|
+
config = ctx.obj['config']
|
|
70
|
+
config_dict = config.to_dict()
|
|
71
|
+
content = click.style(pformat(config_dict), fg='green')
|
|
72
|
+
click.echo(f"File: {config.file_path}\n{content}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@config.command('open')
|
|
76
|
+
@click.pass_context
|
|
77
|
+
@click.option('--config-file', '--config', '-c', is_flag=True, help='Open the config file')
|
|
78
|
+
@click.option('--docker-file', '--docker', '-d', is_flag=True, help='Open the compose.yml file')
|
|
79
|
+
@click.option('--logging-file', '--logging', '-l', is_flag=True, help='Open the logging.yml file')
|
|
80
|
+
@click.option('--default-editor', '-e', is_flag=True, help='Use system default editor ($VISUAL or $EDITOR)')
|
|
81
|
+
@click.argument('editor', required=False)
|
|
82
|
+
def open_config(ctx, config_file, logging_file, docker_file, default_editor, editor):
|
|
83
|
+
"""Opens the config files, e.g. logging.yml, docker-compose.yml.
|
|
84
|
+
|
|
85
|
+
EDITOR is the optional editor command to use (e.g., "code", "vim", "subl").
|
|
86
|
+
If not specified, prints the file path.
|
|
87
|
+
|
|
88
|
+
Examples:
|
|
89
|
+
pfeed config open -l code # Open logging file in VS Code
|
|
90
|
+
pfeed config open -c vim # Open config file in vim
|
|
91
|
+
pfeed config open -d -E # Open docker file in default editor
|
|
92
|
+
pfeed config open -l # Just print the logging file path
|
|
93
|
+
"""
|
|
94
|
+
import subprocess
|
|
95
|
+
|
|
96
|
+
config = ctx.obj['config']
|
|
97
|
+
paths = config._paths
|
|
98
|
+
project_name = paths.project_name
|
|
99
|
+
|
|
100
|
+
if sum([config_file, logging_file, docker_file]) > 1:
|
|
101
|
+
click.echo('Please specify only one file to open')
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Determine which file to open
|
|
105
|
+
if config_file:
|
|
106
|
+
file_path = config.file_path
|
|
107
|
+
elif logging_file:
|
|
108
|
+
file_path = config.logging_config_file_path
|
|
109
|
+
elif docker_file:
|
|
110
|
+
file_path = config.docker_compose_file_path
|
|
111
|
+
else:
|
|
112
|
+
click.echo(f'Please specify a file to open, run "{project_name} config open --help" for more info')
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Handle opening the file
|
|
116
|
+
if default_editor:
|
|
117
|
+
# Use Click's built-in editor (respects $VISUAL/$EDITOR)
|
|
118
|
+
click.edit(filename=str(file_path))
|
|
119
|
+
else:
|
|
120
|
+
# Auto-detect editor if not specified
|
|
121
|
+
editor = editor or auto_detect_editor()
|
|
122
|
+
|
|
123
|
+
if editor:
|
|
124
|
+
try:
|
|
125
|
+
open_file_with_editor(file_path, editor)
|
|
126
|
+
# Get display name for the editor
|
|
127
|
+
editor_names = {
|
|
128
|
+
'cursor': 'Cursor',
|
|
129
|
+
'code': 'VS Code',
|
|
130
|
+
'zed': 'Zed',
|
|
131
|
+
'charm': 'PyCharm',
|
|
132
|
+
'nvim': 'Neovim',
|
|
133
|
+
}
|
|
134
|
+
display_name = editor_names.get(editor, editor)
|
|
135
|
+
click.echo(f"Opened {project_name}'s {file_path.name} with {display_name}")
|
|
136
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
137
|
+
pass # Error already printed by open_file_with_editor
|
|
138
|
+
else:
|
|
139
|
+
# No editor found, print helpful message
|
|
140
|
+
click.echo("No code editor detected.", err=True)
|
|
141
|
+
click.echo(f"Tip: Specify an editor (e.g., '{project_name} config open -l code' to use VS Code) or use -E for system default editor", err=True)
|
|
142
|
+
click.echo(f"\nFile location: {file_path}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@config.command('set')
|
|
146
|
+
@click.pass_context
|
|
147
|
+
@click.option('--data-path', '--data', type=click.Path(resolve_path=True), help='Set the data path')
|
|
148
|
+
@click.option('--log-path', '--log', type=click.Path(resolve_path=True), help='Set the log path')
|
|
149
|
+
@click.option('--cache-path', '--cache', type=click.Path(resolve_path=True), help='Set the cache path')
|
|
150
|
+
def set_config(ctx, data_path, log_path, cache_path):
|
|
151
|
+
"""Update configuration paths.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
pfeed config set --data /path/to/data
|
|
155
|
+
pfeed config set --log /var/log/pfeed --cache /tmp/cache
|
|
156
|
+
"""
|
|
157
|
+
config = ctx.obj['config']
|
|
158
|
+
|
|
159
|
+
# Check if at least one option is provided
|
|
160
|
+
if not any([data_path, log_path, cache_path]):
|
|
161
|
+
click.echo("Error: Please specify at least one path to update.", err=True)
|
|
162
|
+
click.echo("Run 'config set --help' for usage information.")
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Update configuration and track changes
|
|
166
|
+
updated = []
|
|
167
|
+
if data_path:
|
|
168
|
+
config.data_path = data_path
|
|
169
|
+
updated.append(f"data_path -> {data_path}")
|
|
170
|
+
if log_path:
|
|
171
|
+
config.log_path = log_path
|
|
172
|
+
updated.append(f"log_path -> {log_path}")
|
|
173
|
+
if cache_path:
|
|
174
|
+
config.cache_path = cache_path
|
|
175
|
+
updated.append(f"cache_path -> {cache_path}")
|
|
176
|
+
|
|
177
|
+
config.save()
|
|
178
|
+
click.echo(f"Updated {config.filename}:")
|
|
179
|
+
for change in updated:
|
|
180
|
+
click.echo(f" {change}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@config.command()
|
|
184
|
+
@click.pass_context
|
|
185
|
+
@click.option('--config-file', '--config', '-c', is_flag=True, help='Reset the config file')
|
|
186
|
+
@click.option('--logging-file', '--logging', '-l', is_flag=True, help='Reset the logging.yaml file')
|
|
187
|
+
@click.option('--docker-file', '--docker', '-d', is_flag=True, help='Reset the compose.yml file')
|
|
188
|
+
def reset(ctx, config_file, logging_file, docker_file):
|
|
189
|
+
"""Reset the configuration to defaults.
|
|
190
|
+
If no flags were set, all files will be reset.
|
|
191
|
+
Args:
|
|
192
|
+
config_file: Reset the config file
|
|
193
|
+
docker_file: Reset the compose.yml file
|
|
194
|
+
logging_file: Reset the logging.yaml file for logging config
|
|
195
|
+
"""
|
|
196
|
+
config = ctx.obj['config']
|
|
197
|
+
|
|
198
|
+
# If no flags were set, set all to True
|
|
199
|
+
if not any([config_file, logging_file, docker_file]):
|
|
200
|
+
config_file = logging_file = docker_file = True
|
|
201
|
+
|
|
202
|
+
paths = config._paths
|
|
203
|
+
project_name = paths.project_name
|
|
204
|
+
|
|
205
|
+
def _reset_file(filename):
|
|
206
|
+
default_file = paths.package_path / filename
|
|
207
|
+
if not default_file.exists() and paths.project_root:
|
|
208
|
+
default_file = paths.project_root / filename
|
|
209
|
+
|
|
210
|
+
user_file = config.path / filename
|
|
211
|
+
backup_file = config.path / f'{filename}.bak'
|
|
212
|
+
|
|
213
|
+
# Backup existing user file if it exists
|
|
214
|
+
if user_file.exists():
|
|
215
|
+
shutil.copy(user_file, backup_file)
|
|
216
|
+
click.echo(f" Backed up the existing file {user_file.name} to {backup_file.name}")
|
|
217
|
+
|
|
218
|
+
# Copy default file to user location
|
|
219
|
+
if default_file.exists():
|
|
220
|
+
shutil.copy(default_file, user_file)
|
|
221
|
+
click.echo(f" Restored from default file {default_file.name}")
|
|
222
|
+
else:
|
|
223
|
+
click.echo(f" Warning: Default file {default_file.name} not found, skipping", err=True)
|
|
224
|
+
|
|
225
|
+
if config_file:
|
|
226
|
+
filename = config.filename
|
|
227
|
+
click.echo(f"Resetting {project_name}'s {filename}...")
|
|
228
|
+
_reset_file(filename)
|
|
229
|
+
|
|
230
|
+
if logging_file:
|
|
231
|
+
filename = config.LOGGING_CONFIG_FILENAME
|
|
232
|
+
click.echo(f"Resetting {project_name}'s {filename}...")
|
|
233
|
+
_reset_file(filename)
|
|
234
|
+
|
|
235
|
+
if docker_file:
|
|
236
|
+
filename = config.DOCKER_COMPOSE_FILENAME
|
|
237
|
+
click.echo(f"Resetting {project_name}'s {filename}...")
|
|
238
|
+
_reset_file(filename)
|
|
239
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# FIXME: fix it when pfund/pfeed has finished the docs
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import webbrowser
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# FIXME
|
|
10
|
+
PFEED_DOCS_URL = 'https://pfeed-docs.pfund.ai'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _execute_notebooks(docs_path: str):
|
|
14
|
+
"""Clear outputs and execute notebooks"""
|
|
15
|
+
find_ipynb_cmd = ["find", docs_path, "-path", f"{docs_path}/_build", "-prune", "-o", "-name", "*.ipynb", "-print"]
|
|
16
|
+
clear_output_cmd = ["-exec", "jupyter", "nbconvert", "--ClearOutputPreprocessor.enabled=True", "--inplace", "{}", ";"]
|
|
17
|
+
execute_cmd = ["-exec", "jupyter", "nbconvert", "--execute", "--inplace", "{}", ";"]
|
|
18
|
+
try:
|
|
19
|
+
subprocess.run(find_ipynb_cmd + clear_output_cmd, cwd=docs_path, check=True)
|
|
20
|
+
click.echo("Notebook outputs cleared successfully.")
|
|
21
|
+
subprocess.run(find_ipynb_cmd + execute_cmd, cwd=docs_path, check=True)
|
|
22
|
+
click.echo("Notebooks executed successfully.")
|
|
23
|
+
except subprocess.CalledProcessError as e:
|
|
24
|
+
click.echo(f"Error executing notebooks: {e}", err=True)
|
|
25
|
+
raise e
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command()
|
|
29
|
+
@click.option('--build', is_flag=True, is_eager=True, help='Build the docs')
|
|
30
|
+
@click.option('--start', is_flag=True, is_eager=True, help='Start the docs server')
|
|
31
|
+
@click.option('--execute', is_flag=True, help='If True, execute jupyter notebooks')
|
|
32
|
+
def doc(build, start, execute):
|
|
33
|
+
if build and start:
|
|
34
|
+
raise click.UsageError("You can only specify either --build or --start, not both.")
|
|
35
|
+
elif not build and not start:
|
|
36
|
+
if execute:
|
|
37
|
+
raise click.UsageError("You must specify either --build or --start.")
|
|
38
|
+
else:
|
|
39
|
+
webbrowser.open(PFEED_DOCS_URL)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
docs_path = str(MAIN_PATH / 'docs')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
if build:
|
|
46
|
+
clean_cmd = ["myst", "clean", "--all", "--yes"]
|
|
47
|
+
subprocess.run(clean_cmd, cwd=docs_path, check=True)
|
|
48
|
+
click.echo("Docs cleaned successfully.")
|
|
49
|
+
|
|
50
|
+
build_cmd = ["myst", "build", "--html"]
|
|
51
|
+
if execute:
|
|
52
|
+
_execute_notebooks(docs_path)
|
|
53
|
+
subprocess.run(build_cmd, cwd=docs_path, check=True)
|
|
54
|
+
click.echo("Docs built successfully.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if start:
|
|
58
|
+
start_cmd = ["myst", "start"]
|
|
59
|
+
if execute:
|
|
60
|
+
_execute_notebooks(docs_path)
|
|
61
|
+
subprocess.run(start_cmd, cwd=docs_path, check=True)
|
|
62
|
+
return
|
|
63
|
+
except subprocess.CalledProcessError as e:
|
|
64
|
+
click.echo(f"Error using myst: {e}", err=True)
|
|
65
|
+
raise e
|