vyasa 0.3.6__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.
- vyasa/__init__.py +5 -0
- vyasa/agent.py +116 -0
- vyasa/build.py +660 -0
- vyasa/config.py +224 -0
- vyasa/core.py +2825 -0
- vyasa/helpers.py +349 -0
- vyasa/layout_helpers.py +40 -0
- vyasa/main.py +108 -0
- vyasa/static/scripts.js +1202 -0
- vyasa/static/sidenote.css +21 -0
- vyasa-0.3.6.dist-info/METADATA +227 -0
- vyasa-0.3.6.dist-info/RECORD +16 -0
- vyasa-0.3.6.dist-info/WHEEL +5 -0
- vyasa-0.3.6.dist-info/entry_points.txt +2 -0
- vyasa-0.3.6.dist-info/licenses/LICENSE +201 -0
- vyasa-0.3.6.dist-info/top_level.txt +1 -0
vyasa/helpers.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import tomllib
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import frontmatter
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
def slug_to_title(s: str, abbreviations=None) -> str:
|
|
11
|
+
abbreviations = abbreviations or []
|
|
12
|
+
abbrev_set = {str(word).strip().lower() for word in abbreviations if str(word).strip()}
|
|
13
|
+
words = s.replace('-', ' ').replace('_', ' ').split()
|
|
14
|
+
titled = []
|
|
15
|
+
for word in words:
|
|
16
|
+
lowered = word.lower()
|
|
17
|
+
if lowered in abbrev_set:
|
|
18
|
+
titled.append(word.upper())
|
|
19
|
+
elif word.isupper():
|
|
20
|
+
titled.append(word)
|
|
21
|
+
else:
|
|
22
|
+
titled.append(word[0].upper() + word[1:])
|
|
23
|
+
return ' '.join(titled)
|
|
24
|
+
|
|
25
|
+
def _strip_inline_markdown(text: str) -> str:
|
|
26
|
+
cleaned = text or ""
|
|
27
|
+
cleaned = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', cleaned)
|
|
28
|
+
cleaned = re.sub(r'\[([^\]]+)\]\[[^\]]*\]', r'\1', cleaned)
|
|
29
|
+
cleaned = re.sub(r'`([^`]+)`', r'\1', cleaned)
|
|
30
|
+
cleaned = re.sub(r'\*\*([^*]+)\*\*', r'\1', cleaned)
|
|
31
|
+
cleaned = re.sub(r'__([^_]+)__', r'\1', cleaned)
|
|
32
|
+
cleaned = re.sub(r'\*([^*]+)\*', r'\1', cleaned)
|
|
33
|
+
cleaned = re.sub(r'_([^_]+)_', r'\1', cleaned)
|
|
34
|
+
cleaned = re.sub(r'~~([^~]+)~~', r'\1', cleaned)
|
|
35
|
+
return cleaned
|
|
36
|
+
|
|
37
|
+
def _plain_text_from_html(text: str) -> str:
|
|
38
|
+
import html
|
|
39
|
+
cleaned = re.sub(r'<[^>]+>', '', text or "")
|
|
40
|
+
return html.unescape(cleaned)
|
|
41
|
+
|
|
42
|
+
def text_to_anchor(text: str) -> str:
|
|
43
|
+
"""Convert text to anchor slug"""
|
|
44
|
+
cleaned = _strip_inline_markdown(text)
|
|
45
|
+
cleaned = _plain_text_from_html(cleaned)
|
|
46
|
+
return re.sub(r'[^\w\s-]', '', cleaned.lower()).replace(' ', '-')
|
|
47
|
+
|
|
48
|
+
def _unique_anchor(base: str, counts: dict[str, int]) -> str:
|
|
49
|
+
if not base:
|
|
50
|
+
base = "section"
|
|
51
|
+
current = counts.get(base, 0) + 1
|
|
52
|
+
counts[base] = current
|
|
53
|
+
return base if current == 1 else f"{base}-{current}"
|
|
54
|
+
|
|
55
|
+
_frontmatter_cache: dict[str, tuple[float, tuple[dict, str]]] = {}
|
|
56
|
+
|
|
57
|
+
def parse_frontmatter(file_path: str | Path):
|
|
58
|
+
"""Parse frontmatter from a markdown file with caching"""
|
|
59
|
+
import time
|
|
60
|
+
start_time = time.time()
|
|
61
|
+
|
|
62
|
+
file_path = Path(file_path)
|
|
63
|
+
cache_key = str(file_path)
|
|
64
|
+
mtime = file_path.stat().st_mtime
|
|
65
|
+
|
|
66
|
+
if cache_key in _frontmatter_cache:
|
|
67
|
+
cached_mtime, cached_data = _frontmatter_cache[cache_key]
|
|
68
|
+
if cached_mtime == mtime:
|
|
69
|
+
elapsed = (time.time() - start_time) * 1000
|
|
70
|
+
logger.debug(f"[DEBUG] parse_frontmatter CACHE HIT for {file_path.name} ({elapsed:.2f}ms)")
|
|
71
|
+
return cached_data
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
75
|
+
post = frontmatter.load(f)
|
|
76
|
+
result = (post.metadata, post.content)
|
|
77
|
+
_frontmatter_cache[cache_key] = (mtime, result)
|
|
78
|
+
elapsed = (time.time() - start_time) * 1000
|
|
79
|
+
logger.debug(f"[DEBUG] parse_frontmatter READ FILE {file_path.name} ({elapsed:.2f}ms)")
|
|
80
|
+
return result
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"Error parsing frontmatter from {file_path}: {e}")
|
|
83
|
+
return {}, open(file_path).read()
|
|
84
|
+
|
|
85
|
+
def get_post_title(file_path: str | Path, abbreviations=None) -> str:
|
|
86
|
+
"""Get post title from frontmatter or filename"""
|
|
87
|
+
metadata, _ = parse_frontmatter(file_path)
|
|
88
|
+
file_path = Path(file_path)
|
|
89
|
+
return metadata.get('title', slug_to_title(file_path.stem, abbreviations=abbreviations))
|
|
90
|
+
|
|
91
|
+
@lru_cache(maxsize=128)
|
|
92
|
+
def _cached_vyasa_config(path_str: str, mtime: float):
|
|
93
|
+
path = Path(path_str)
|
|
94
|
+
try:
|
|
95
|
+
with path.open("rb") as f:
|
|
96
|
+
return tomllib.load(f)
|
|
97
|
+
except Exception:
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
def _normalize_vyasa_config(parsed):
|
|
101
|
+
config = {
|
|
102
|
+
"order": [],
|
|
103
|
+
"sort": "name_asc",
|
|
104
|
+
"folders_first": True,
|
|
105
|
+
"folders_always_first": False,
|
|
106
|
+
"layout_max_width": None,
|
|
107
|
+
"abbreviations": None,
|
|
108
|
+
}
|
|
109
|
+
if not isinstance(parsed, dict):
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
order = parsed.get("order")
|
|
113
|
+
if order is not None:
|
|
114
|
+
if isinstance(order, (list, tuple)):
|
|
115
|
+
config["order"] = [str(item).strip() for item in order if str(item).strip()]
|
|
116
|
+
else:
|
|
117
|
+
config["order"] = []
|
|
118
|
+
|
|
119
|
+
sort = parsed.get("sort")
|
|
120
|
+
if isinstance(sort, str) and sort in ("name_asc", "name_desc", "mtime_asc", "mtime_desc"):
|
|
121
|
+
config["sort"] = sort
|
|
122
|
+
|
|
123
|
+
folders_first = parsed.get("folders_first")
|
|
124
|
+
if isinstance(folders_first, bool):
|
|
125
|
+
config["folders_first"] = folders_first
|
|
126
|
+
elif isinstance(folders_first, str):
|
|
127
|
+
lowered = folders_first.lower()
|
|
128
|
+
if lowered in ("true", "false"):
|
|
129
|
+
config["folders_first"] = lowered == "true"
|
|
130
|
+
|
|
131
|
+
folders_always_first = parsed.get("folders_always_first")
|
|
132
|
+
if isinstance(folders_always_first, bool):
|
|
133
|
+
config["folders_always_first"] = folders_always_first
|
|
134
|
+
elif isinstance(folders_always_first, str):
|
|
135
|
+
lowered = folders_always_first.lower()
|
|
136
|
+
if lowered in ("true", "false"):
|
|
137
|
+
config["folders_always_first"] = lowered == "true"
|
|
138
|
+
|
|
139
|
+
for key in ("layout_max_width",):
|
|
140
|
+
value = parsed.get(key)
|
|
141
|
+
if isinstance(value, (int, float)):
|
|
142
|
+
value = str(value)
|
|
143
|
+
if isinstance(value, str):
|
|
144
|
+
value = value.strip()
|
|
145
|
+
config[key] = value if value else None
|
|
146
|
+
|
|
147
|
+
abbreviations = parsed.get("abbreviations")
|
|
148
|
+
if isinstance(abbreviations, (list, tuple, set)):
|
|
149
|
+
config["abbreviations"] = [str(item).strip() for item in abbreviations if str(item).strip()]
|
|
150
|
+
elif isinstance(abbreviations, str):
|
|
151
|
+
parts = [part.strip() for part in abbreviations.split(",")]
|
|
152
|
+
config["abbreviations"] = [part for part in parts if part]
|
|
153
|
+
|
|
154
|
+
return config
|
|
155
|
+
|
|
156
|
+
def _effective_abbreviations(root: Path, folder: Path | None = None):
|
|
157
|
+
root_config = get_vyasa_config(root)
|
|
158
|
+
root_abbrevs = root_config.get("abbreviations") or []
|
|
159
|
+
if folder is None or folder == root:
|
|
160
|
+
return root_abbrevs
|
|
161
|
+
folder_config = get_vyasa_config(folder)
|
|
162
|
+
folder_abbrevs = folder_config.get("abbreviations")
|
|
163
|
+
return folder_abbrevs if folder_abbrevs is not None else root_abbrevs
|
|
164
|
+
|
|
165
|
+
def get_vyasa_config(folder: Path):
|
|
166
|
+
vyasa_path = folder / ".vyasa"
|
|
167
|
+
if not vyasa_path.exists():
|
|
168
|
+
return _normalize_vyasa_config({})
|
|
169
|
+
try:
|
|
170
|
+
mtime = vyasa_path.stat().st_mtime
|
|
171
|
+
except OSError:
|
|
172
|
+
return _normalize_vyasa_config({})
|
|
173
|
+
parsed = _cached_vyasa_config(str(vyasa_path), mtime)
|
|
174
|
+
config = _normalize_vyasa_config(parsed)
|
|
175
|
+
logger.debug(
|
|
176
|
+
"[DEBUG] .vyasa config for %s: order=%s sort=%s folders_first=%s",
|
|
177
|
+
folder,
|
|
178
|
+
config.get("order"),
|
|
179
|
+
config.get("sort"),
|
|
180
|
+
config.get("folders_first"),
|
|
181
|
+
)
|
|
182
|
+
return config
|
|
183
|
+
|
|
184
|
+
def order_vyasa_entries(entries, config):
|
|
185
|
+
if not entries:
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
order_list = [name.strip().rstrip("/") for name in config.get("order", []) if str(name).strip()]
|
|
189
|
+
if not order_list:
|
|
190
|
+
sorted_entries = _sort_vyasa_entries(entries, config.get("sort"), config.get("folders_first", True))
|
|
191
|
+
if config.get("folders_always_first"):
|
|
192
|
+
sorted_entries = _group_folders_first(sorted_entries)
|
|
193
|
+
logger.debug(
|
|
194
|
+
"[DEBUG] .vyasa order empty; sorted entries: %s",
|
|
195
|
+
[item.name for item in sorted_entries],
|
|
196
|
+
)
|
|
197
|
+
return sorted_entries
|
|
198
|
+
|
|
199
|
+
exact_map = {}
|
|
200
|
+
stem_map = {}
|
|
201
|
+
for item in entries:
|
|
202
|
+
exact_map.setdefault(item.name, item)
|
|
203
|
+
if item.suffix == ".md":
|
|
204
|
+
stem_map.setdefault(item.stem, item)
|
|
205
|
+
|
|
206
|
+
ordered = []
|
|
207
|
+
used = set()
|
|
208
|
+
for name in order_list:
|
|
209
|
+
if name in exact_map:
|
|
210
|
+
item = exact_map[name]
|
|
211
|
+
elif name in stem_map:
|
|
212
|
+
item = stem_map[name]
|
|
213
|
+
else:
|
|
214
|
+
item = None
|
|
215
|
+
if item and item not in used:
|
|
216
|
+
ordered.append(item)
|
|
217
|
+
used.add(item)
|
|
218
|
+
|
|
219
|
+
remaining = [item for item in entries if item not in used]
|
|
220
|
+
remaining_sorted = _sort_vyasa_entries(
|
|
221
|
+
remaining,
|
|
222
|
+
config.get("sort"),
|
|
223
|
+
config.get("folders_first", True)
|
|
224
|
+
)
|
|
225
|
+
combined = ordered + remaining_sorted
|
|
226
|
+
if config.get("folders_always_first"):
|
|
227
|
+
combined = _group_folders_first(combined)
|
|
228
|
+
logger.debug(
|
|
229
|
+
"[DEBUG] .vyasa ordered=%s remaining=%s",
|
|
230
|
+
[item.name for item in ordered],
|
|
231
|
+
[item.name for item in remaining_sorted],
|
|
232
|
+
)
|
|
233
|
+
return combined
|
|
234
|
+
|
|
235
|
+
def _group_folders_first(entries):
|
|
236
|
+
folders = [item for item in entries if item.is_dir()]
|
|
237
|
+
files = [item for item in entries if not item.is_dir()]
|
|
238
|
+
return folders + files
|
|
239
|
+
|
|
240
|
+
def _sort_vyasa_entries(entries, sort_method, folders_first):
|
|
241
|
+
method = sort_method or "name_asc"
|
|
242
|
+
reverse = method.endswith("desc")
|
|
243
|
+
by_mtime = method.startswith("mtime")
|
|
244
|
+
|
|
245
|
+
def sort_key(item):
|
|
246
|
+
if by_mtime:
|
|
247
|
+
try:
|
|
248
|
+
return item.stat().st_mtime
|
|
249
|
+
except OSError:
|
|
250
|
+
return 0
|
|
251
|
+
return item.name.lower()
|
|
252
|
+
|
|
253
|
+
if folders_first:
|
|
254
|
+
folders = [item for item in entries if item.is_dir()]
|
|
255
|
+
files = [item for item in entries if not item.is_dir()]
|
|
256
|
+
folders_sorted = sorted(folders, key=sort_key, reverse=reverse)
|
|
257
|
+
files_sorted = sorted(files, key=sort_key, reverse=reverse)
|
|
258
|
+
return folders_sorted + files_sorted
|
|
259
|
+
|
|
260
|
+
return sorted(entries, key=sort_key, reverse=reverse)
|
|
261
|
+
|
|
262
|
+
def list_vyasa_posts(root: Path, include_hidden: bool = False) -> list[dict]:
|
|
263
|
+
"""List all posts in the blog root (md + pdf)."""
|
|
264
|
+
root = root.resolve()
|
|
265
|
+
root_parts = len(root.parts)
|
|
266
|
+
posts: list[dict] = []
|
|
267
|
+
abbreviations = _effective_abbreviations(root)
|
|
268
|
+
|
|
269
|
+
for path in sorted(root.rglob("*")):
|
|
270
|
+
if not path.is_file():
|
|
271
|
+
continue
|
|
272
|
+
rel_parts = path.parts[root_parts:]
|
|
273
|
+
if not rel_parts:
|
|
274
|
+
continue
|
|
275
|
+
if not include_hidden and any(part.startswith(".") for part in rel_parts):
|
|
276
|
+
continue
|
|
277
|
+
if path.suffix.lower() not in {".md", ".pdf"}:
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
rel = Path(*rel_parts)
|
|
281
|
+
slug = rel.with_suffix("").as_posix()
|
|
282
|
+
if path.suffix.lower() == ".md":
|
|
283
|
+
title = get_post_title(path, abbreviations=abbreviations)
|
|
284
|
+
kind = "md"
|
|
285
|
+
else:
|
|
286
|
+
title = slug_to_title(rel.stem, abbreviations=abbreviations)
|
|
287
|
+
kind = "pdf"
|
|
288
|
+
|
|
289
|
+
posts.append(
|
|
290
|
+
{
|
|
291
|
+
"path": slug,
|
|
292
|
+
"title": title,
|
|
293
|
+
"type": kind,
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return posts
|
|
298
|
+
|
|
299
|
+
def list_vyasa_entries(root: Path, relative: str = ".", include_hidden: bool = False) -> dict:
|
|
300
|
+
"""List immediate entries (folders + md/pdf files) under a relative path."""
|
|
301
|
+
root = root.resolve()
|
|
302
|
+
target = (root / relative).resolve()
|
|
303
|
+
if target != root and root not in target.parents:
|
|
304
|
+
return {"error": "Path escapes blog root"}
|
|
305
|
+
if not target.exists() or not target.is_dir():
|
|
306
|
+
return {"error": "Folder not found"}
|
|
307
|
+
|
|
308
|
+
abbreviations = _effective_abbreviations(root, target)
|
|
309
|
+
entries: list[dict] = []
|
|
310
|
+
for item in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
|
|
311
|
+
if not include_hidden and item.name.startswith("."):
|
|
312
|
+
continue
|
|
313
|
+
if item.is_dir():
|
|
314
|
+
entries.append({"type": "folder", "path": item.relative_to(root).as_posix()})
|
|
315
|
+
continue
|
|
316
|
+
if item.suffix.lower() not in {".md", ".pdf"}:
|
|
317
|
+
continue
|
|
318
|
+
rel = item.relative_to(root)
|
|
319
|
+
slug = rel.with_suffix("").as_posix()
|
|
320
|
+
if item.suffix.lower() == ".md":
|
|
321
|
+
title = get_post_title(item, abbreviations=abbreviations)
|
|
322
|
+
kind = "md"
|
|
323
|
+
else:
|
|
324
|
+
title = slug_to_title(rel.stem, abbreviations=abbreviations)
|
|
325
|
+
kind = "pdf"
|
|
326
|
+
entries.append({"type": kind, "path": slug, "title": title})
|
|
327
|
+
|
|
328
|
+
return {"path": target.relative_to(root).as_posix(), "entries": entries}
|
|
329
|
+
|
|
330
|
+
def find_folder_note_file(folder: Path) -> Path | None:
|
|
331
|
+
"""Return the preferred folder note file (index.md, readme.md, or foldername.md)."""
|
|
332
|
+
try:
|
|
333
|
+
folder_name = folder.name.lower()
|
|
334
|
+
index_file = None
|
|
335
|
+
readme_file = None
|
|
336
|
+
named_file = None
|
|
337
|
+
for item in folder.iterdir():
|
|
338
|
+
if not item.is_file() or item.suffix.lower() != ".md":
|
|
339
|
+
continue
|
|
340
|
+
stem = item.stem.lower()
|
|
341
|
+
if stem == "index":
|
|
342
|
+
index_file = item
|
|
343
|
+
elif stem == "readme":
|
|
344
|
+
readme_file = item
|
|
345
|
+
elif stem == folder_name:
|
|
346
|
+
named_file = item
|
|
347
|
+
return index_file or readme_file or named_file
|
|
348
|
+
except OSError:
|
|
349
|
+
return None
|
vyasa/layout_helpers.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from .config import get_config
|
|
5
|
+
|
|
6
|
+
def _coerce_config_str(value):
|
|
7
|
+
if value is None:
|
|
8
|
+
return None
|
|
9
|
+
if isinstance(value, (int, float)):
|
|
10
|
+
return str(value)
|
|
11
|
+
if isinstance(value, str):
|
|
12
|
+
cleaned = value.strip()
|
|
13
|
+
return cleaned if cleaned else None
|
|
14
|
+
return str(value)
|
|
15
|
+
|
|
16
|
+
def _width_class_and_style(value, kind):
|
|
17
|
+
if not value:
|
|
18
|
+
return "", ""
|
|
19
|
+
val = value.strip()
|
|
20
|
+
lowered = val.lower()
|
|
21
|
+
if lowered in ("default", "auto", "none"):
|
|
22
|
+
return "", ""
|
|
23
|
+
if kind == "max":
|
|
24
|
+
if val.startswith("max-w-"):
|
|
25
|
+
return val, ""
|
|
26
|
+
if re.match(r'^\d+(\.\d+)?$', val):
|
|
27
|
+
val = f"{val}px"
|
|
28
|
+
return "", f"--layout-max-width: {val};"
|
|
29
|
+
return "", ""
|
|
30
|
+
|
|
31
|
+
def _style_attr(style_value):
|
|
32
|
+
if not style_value:
|
|
33
|
+
return {}
|
|
34
|
+
return {"style": style_value}
|
|
35
|
+
|
|
36
|
+
def _resolve_layout_config(current_path):
|
|
37
|
+
config = get_config()
|
|
38
|
+
return {
|
|
39
|
+
"layout_max_width": _coerce_config_str(config.get("layout_max_width", "VYASA_LAYOUT_MAX_WIDTH", "75vw")),
|
|
40
|
+
}
|
vyasa/main.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from .config import get_config, reload_config
|
|
5
|
+
|
|
6
|
+
# Import app at module level, but config will be initialized before it's used
|
|
7
|
+
from .core import app
|
|
8
|
+
|
|
9
|
+
def build_command():
|
|
10
|
+
"""CLI entry point for vyasa build command"""
|
|
11
|
+
import argparse
|
|
12
|
+
from .build import build_static_site
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(description='Build static site from markdown files')
|
|
15
|
+
parser.add_argument('directory', nargs='?', help='Path to markdown files directory')
|
|
16
|
+
parser.add_argument('-o', '--output', help='Output directory (default: ./dist)', default='dist')
|
|
17
|
+
|
|
18
|
+
args = parser.parse_args(sys.argv[2:]) # Skip 'vyasa' and 'build'
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
output_dir = build_static_site(input_dir=args.directory, output_dir=args.output)
|
|
22
|
+
return 0
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Error building static site: {e}", file=sys.stderr)
|
|
25
|
+
import traceback
|
|
26
|
+
traceback.print_exc()
|
|
27
|
+
return 1
|
|
28
|
+
|
|
29
|
+
def cli():
|
|
30
|
+
"""CLI entry point for vyasa command
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
vyasa [directory] # Run locally on 127.0.0.1:5001
|
|
34
|
+
vyasa [directory] --host 0.0.0.0 # Run on all interfaces
|
|
35
|
+
vyasa build [directory] # Build static site
|
|
36
|
+
vyasa build [directory] -o output # Build to custom output directory
|
|
37
|
+
|
|
38
|
+
Environment variables:
|
|
39
|
+
VYASA_ROOT: Path to markdown files
|
|
40
|
+
VYASA_HOST: Server host (default: 127.0.0.1)
|
|
41
|
+
VYASA_PORT: Server port (default: 5001)
|
|
42
|
+
|
|
43
|
+
Configuration file:
|
|
44
|
+
Create a .vyasa file (TOML format) in your blog directory
|
|
45
|
+
"""
|
|
46
|
+
import uvicorn
|
|
47
|
+
import argparse
|
|
48
|
+
|
|
49
|
+
# Check if first argument is 'build'
|
|
50
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'build':
|
|
51
|
+
sys.exit(build_command())
|
|
52
|
+
|
|
53
|
+
parser = argparse.ArgumentParser(description='Run Vyasa server')
|
|
54
|
+
parser.add_argument('directory', nargs='?', help='Path to markdown files directory')
|
|
55
|
+
parser.add_argument('--host', help='Server host (default: 127.0.0.1, use 0.0.0.0 for all interfaces)')
|
|
56
|
+
parser.add_argument('--port', type=int, help='Server port (default: 5001)')
|
|
57
|
+
parser.add_argument('--no-reload', action='store_true', help='Disable auto-reload')
|
|
58
|
+
parser.add_argument('--user', help='Login username (overrides config/env)')
|
|
59
|
+
parser.add_argument('--password', help='Login password (overrides config/env)')
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
# Set root folder from arguments or environment
|
|
64
|
+
if args.directory:
|
|
65
|
+
root = Path(args.directory).resolve()
|
|
66
|
+
if not root.exists():
|
|
67
|
+
print(f"Error: Directory {root} does not exist")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
os.environ['VYASA_ROOT'] = str(root)
|
|
70
|
+
|
|
71
|
+
# Initialize or reload config to pick up .vyasa file
|
|
72
|
+
# This ensures .vyasa file is loaded and config is refreshed
|
|
73
|
+
config = reload_config() if args.directory else get_config()
|
|
74
|
+
|
|
75
|
+
# Get host and port from arguments, config, or use defaults
|
|
76
|
+
host = args.host or config.get_host()
|
|
77
|
+
port = args.port or config.get_port()
|
|
78
|
+
reload = not args.no_reload
|
|
79
|
+
|
|
80
|
+
# Set login credentials from CLI if provided
|
|
81
|
+
if args.user:
|
|
82
|
+
os.environ['VYASA_USER'] = args.user
|
|
83
|
+
if args.password:
|
|
84
|
+
os.environ['VYASA_PASSWORD'] = args.password
|
|
85
|
+
|
|
86
|
+
print(f"Starting Vyasa server...")
|
|
87
|
+
print(f"Blog root: {config.get_root_folder()}")
|
|
88
|
+
print(f"Blog title: {config.get_blog_title()}")
|
|
89
|
+
print(f"Serving at: http://{host}:{port}")
|
|
90
|
+
if host == '0.0.0.0':
|
|
91
|
+
print(f"Server accessible from network at: http://<your-ip>:{port}")
|
|
92
|
+
|
|
93
|
+
# Configure reload to watch markdown and PDF files in the blog directory
|
|
94
|
+
reload_kwargs = {}
|
|
95
|
+
if reload:
|
|
96
|
+
blog_root = config.get_root_folder()
|
|
97
|
+
reload_kwargs = {
|
|
98
|
+
"reload": True,
|
|
99
|
+
"reload_dirs": [str(blog_root)],
|
|
100
|
+
"reload_includes": ["*.md", "*.pdf", "*.vyasa"]
|
|
101
|
+
}
|
|
102
|
+
else:
|
|
103
|
+
reload_kwargs = {"reload": False}
|
|
104
|
+
|
|
105
|
+
uvicorn.run("vyasa.main:app", host=host, port=port, **reload_kwargs)
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
cli()
|