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.
Files changed (41) hide show
  1. pfund_kit-0.1.0/PKG-INFO +24 -0
  2. pfund_kit-0.1.0/README.md +2 -0
  3. pfund_kit-0.1.0/pyproject.toml +38 -0
  4. pfund_kit-0.1.0/src/pfund_kit/__init__.py +5 -0
  5. pfund_kit-0.1.0/src/pfund_kit/aliase.py +283 -0
  6. pfund_kit-0.1.0/src/pfund_kit/cli/__init__.py +4 -0
  7. pfund_kit-0.1.0/src/pfund_kit/cli/commands/__init__.py +12 -0
  8. pfund_kit-0.1.0/src/pfund_kit/cli/commands/config.py +239 -0
  9. pfund_kit-0.1.0/src/pfund_kit/cli/commands/doc.py +65 -0
  10. pfund_kit-0.1.0/src/pfund_kit/cli/commands/docker_compose.py +54 -0
  11. pfund_kit-0.1.0/src/pfund_kit/cli/commands/remove.py +112 -0
  12. pfund_kit-0.1.0/src/pfund_kit/cli/main.py +39 -0
  13. pfund_kit-0.1.0/src/pfund_kit/cli/utils.py +46 -0
  14. pfund_kit-0.1.0/src/pfund_kit/config.py +194 -0
  15. pfund_kit-0.1.0/src/pfund_kit/enums/notebook_type.py +7 -0
  16. pfund_kit-0.1.0/src/pfund_kit/logging/__init__.py +162 -0
  17. pfund_kit-0.1.0/src/pfund_kit/logging/configurator.py +105 -0
  18. pfund_kit-0.1.0/src/pfund_kit/logging/filters/__init__.py +6 -0
  19. pfund_kit-0.1.0/src/pfund_kit/logging/filters/trimmed_path_filter.py +67 -0
  20. pfund_kit-0.1.0/src/pfund_kit/logging/formatters/__init__.py +6 -0
  21. pfund_kit-0.1.0/src/pfund_kit/logging/formatters/colored_formatter.py +17 -0
  22. pfund_kit-0.1.0/src/pfund_kit/logging/handlers/__init__.py +8 -0
  23. pfund_kit-0.1.0/src/pfund_kit/logging/handlers/compressed_timed_rotating_file_handler.py +104 -0
  24. pfund_kit-0.1.0/src/pfund_kit/logging/handlers/lazy_handler.py +155 -0
  25. pfund_kit-0.1.0/src/pfund_kit/logging/handlers/telegram_handler.py +28 -0
  26. pfund_kit-0.1.0/src/pfund_kit/paths.py +88 -0
  27. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/help.py +30 -0
  28. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/main.py +203 -0
  29. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/shell_group.py +64 -0
  30. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/toolbar.py +154 -0
  31. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/tutorial.py +44 -0
  32. pfund_kit-0.1.0/src/pfund_kit/pfund_shell/utils.py +140 -0
  33. pfund_kit-0.1.0/src/pfund_kit/style.py +291 -0
  34. pfund_kit-0.1.0/src/pfund_kit/utils/__init__.py +238 -0
  35. pfund_kit-0.1.0/src/pfund_kit/utils/function.py +51 -0
  36. pfund_kit-0.1.0/src/pfund_kit/utils/progress_bar.py +286 -0
  37. pfund_kit-0.1.0/src/pfund_kit/utils/singleton.py +22 -0
  38. pfund_kit-0.1.0/src/pfund_kit/utils/temporal.py +71 -0
  39. pfund_kit-0.1.0/src/pfund_kit/utils/text.py +26 -0
  40. pfund_kit-0.1.0/src/pfund_kit/utils/toml.py +176 -0
  41. pfund_kit-0.1.0/src/pfund_kit/utils/yaml.py +171 -0
@@ -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,2 @@
1
+ # pfund-kit
2
+ 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,5 @@
1
+ from importlib.metadata import version
2
+
3
+
4
+ __version__ = version("pfund-kit")
5
+ # __all__ = ()
@@ -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,4 @@
1
+ from pfund_kit.cli.main import create_cli_group
2
+
3
+
4
+ __all__ = ['create_cli_group']
@@ -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