syntaxmatrix 2.5.6__py3-none-any.whl → 2.6.2__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.
- syntaxmatrix/agentic/agents.py +1220 -169
- syntaxmatrix/agentic/agents_orchestrer.py +326 -0
- syntaxmatrix/agentic/code_tools_registry.py +27 -32
- syntaxmatrix/commentary.py +16 -16
- syntaxmatrix/core.py +185 -81
- syntaxmatrix/db.py +460 -4
- syntaxmatrix/{display.py → display_html.py} +2 -6
- syntaxmatrix/gpt_models_latest.py +1 -1
- syntaxmatrix/media/__init__.py +0 -0
- syntaxmatrix/media/media_pixabay.py +277 -0
- syntaxmatrix/models.py +1 -1
- syntaxmatrix/page_builder_defaults.py +183 -0
- syntaxmatrix/page_builder_generation.py +1122 -0
- syntaxmatrix/page_layout_contract.py +644 -0
- syntaxmatrix/page_patch_publish.py +1471 -0
- syntaxmatrix/preface.py +142 -21
- syntaxmatrix/profiles.py +28 -10
- syntaxmatrix/routes.py +1740 -453
- syntaxmatrix/selftest_page_templates.py +360 -0
- syntaxmatrix/settings/client_items.py +28 -0
- syntaxmatrix/settings/model_map.py +1022 -207
- syntaxmatrix/settings/prompts.py +328 -130
- syntaxmatrix/static/assets/hero-default.svg +22 -0
- syntaxmatrix/static/icons/bot-icon.png +0 -0
- syntaxmatrix/static/icons/favicon.png +0 -0
- syntaxmatrix/static/icons/logo.png +0 -0
- syntaxmatrix/static/icons/logo3.png +0 -0
- syntaxmatrix/templates/admin_branding.html +104 -0
- syntaxmatrix/templates/admin_features.html +63 -0
- syntaxmatrix/templates/admin_secretes.html +108 -0
- syntaxmatrix/templates/dashboard.html +296 -133
- syntaxmatrix/templates/dataset_resize.html +535 -0
- syntaxmatrix/templates/edit_page.html +2535 -0
- syntaxmatrix/utils.py +2431 -2383
- {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/METADATA +6 -2
- {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/RECORD +39 -24
- syntaxmatrix/generate_page.py +0 -644
- syntaxmatrix/static/icons/hero_bg.jpg +0 -0
- {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/WHEEL +0 -0
- {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/licenses/LICENSE.txt +0 -0
- {syntaxmatrix-2.5.6.dist-info → syntaxmatrix-2.6.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# syntaxmatrix/selftest_page_templates.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
# Your new contract utilities (from the previous step)
|
|
10
|
+
from syntaxmatrix.page_layout_contract import (
|
|
11
|
+
normalise_layout,
|
|
12
|
+
validate_layout,
|
|
13
|
+
validate_compiled_html,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Your existing patch-only publisher
|
|
17
|
+
from syntaxmatrix.page_patch_publish import patch_page_publish
|
|
18
|
+
|
|
19
|
+
def _pick_section_for_title_patch(sections):
|
|
20
|
+
"""
|
|
21
|
+
Prefer a non-hero section that has both title+text, so we can assert the intro paragraph.
|
|
22
|
+
Fallback: first non-hero section.
|
|
23
|
+
"""
|
|
24
|
+
non_hero = [s for s in sections if isinstance(s, dict) and (s.get("type") or "").lower() != "hero"]
|
|
25
|
+
for s in non_hero:
|
|
26
|
+
if (s.get("title") or "").strip() and (s.get("text") or "").strip():
|
|
27
|
+
return s
|
|
28
|
+
return non_hero[0] if non_hero else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _pick_section_for_item_patch(sections):
|
|
32
|
+
"""
|
|
33
|
+
Prefer a grid-ish section with at least one item.
|
|
34
|
+
"""
|
|
35
|
+
grid_types = {"values", "team", "logos", "testimonials", "faq", "cta",
|
|
36
|
+
"services", "offers", "comparison", "process", "proof", "case_studies"}
|
|
37
|
+
for s in sections:
|
|
38
|
+
if not isinstance(s, dict):
|
|
39
|
+
continue
|
|
40
|
+
st = (s.get("type") or "").lower()
|
|
41
|
+
items = s.get("items") if isinstance(s.get("items"), list) else []
|
|
42
|
+
if st in grid_types and len(items) > 0:
|
|
43
|
+
return s
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _default_fixture_about_v1() -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"page": "about",
|
|
50
|
+
"category": "about",
|
|
51
|
+
"template": {"id": "about_glass_hero_v1", "version": "1.0.0"},
|
|
52
|
+
"meta": {
|
|
53
|
+
"pageTitle": "About SyntaxMatrix",
|
|
54
|
+
"summary": "AI platform framework for developer teams to provision client-ready AI platforms.",
|
|
55
|
+
"primaryCta": {"label": "Request a demo", "href": "#sec_cta"},
|
|
56
|
+
"secondaryCta": {"label": "See capabilities", "href": "#sec_values"},
|
|
57
|
+
},
|
|
58
|
+
"sections": [
|
|
59
|
+
{
|
|
60
|
+
"id": "sec_hero",
|
|
61
|
+
"type": "hero",
|
|
62
|
+
"title": "Build client-ready AI platforms with confidence",
|
|
63
|
+
"text": "SyntaxMatrix helps teams provision, customise, and ship robust AI products faster.",
|
|
64
|
+
"imageUrl": "https://example.com/hero-a.jpg",
|
|
65
|
+
"items": [],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "sec_story",
|
|
69
|
+
"type": "story",
|
|
70
|
+
"title": "Our story",
|
|
71
|
+
"text": "We built SyntaxMatrix to remove repetitive engineering work that slows down AI delivery.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "sec_values",
|
|
75
|
+
"type": "values",
|
|
76
|
+
"title": "What we stand for",
|
|
77
|
+
"text": "Principles that guide how we design and ship.",
|
|
78
|
+
"cols": 3,
|
|
79
|
+
"items": [
|
|
80
|
+
{
|
|
81
|
+
"id": "val_1",
|
|
82
|
+
"type": "card",
|
|
83
|
+
"title": "Engineering rigour",
|
|
84
|
+
"text": "Clear architecture, safe defaults, predictable behaviour.",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "val_2",
|
|
88
|
+
"type": "card",
|
|
89
|
+
"title": "Client readiness",
|
|
90
|
+
"text": "Provisioning, admin tooling, audit trails, deployment patterns.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "val_3",
|
|
94
|
+
"type": "card",
|
|
95
|
+
"title": "Practical AI",
|
|
96
|
+
"text": "RAG, analytics, and automation that supports real teams.",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "sec_cta",
|
|
102
|
+
"type": "cta",
|
|
103
|
+
"title": "Ready to talk?",
|
|
104
|
+
"text": "Tell us what you’re building and we’ll suggest a path to a production-ready setup.",
|
|
105
|
+
"cols": 2,
|
|
106
|
+
"items": [
|
|
107
|
+
{"id": "cta_1", "type": "card", "title": "Book a demo", "text": "See it in action."},
|
|
108
|
+
{"id": "cta_2", "type": "card", "title": "Discuss requirements", "text": "We’ll map your use-case."},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _compile_html(layout: Dict[str, Any]) -> Tuple[str, str]:
|
|
116
|
+
"""
|
|
117
|
+
Prefer page_builder first (lighter deps). Print real import errors if compilers fail.
|
|
118
|
+
Returns: (html, compiler_name)
|
|
119
|
+
"""
|
|
120
|
+
import importlib
|
|
121
|
+
import traceback
|
|
122
|
+
|
|
123
|
+
errors = []
|
|
124
|
+
|
|
125
|
+
# 1) Page builder compiler (preferred)
|
|
126
|
+
try:
|
|
127
|
+
mod = importlib.import_module("syntaxmatrix.page_builder_generation")
|
|
128
|
+
fn = getattr(mod, "compile_layout_to_html", None)
|
|
129
|
+
if callable(fn):
|
|
130
|
+
slug = (layout.get("page") or layout.get("category") or "page")
|
|
131
|
+
return fn(layout, page_slug=str(slug)), "page_builder.compile_layout_to_html"
|
|
132
|
+
errors.append("page_builder: compile_layout_to_html not found/callable")
|
|
133
|
+
except Exception:
|
|
134
|
+
errors.append("page_builder import failed:\n" + traceback.format_exc())
|
|
135
|
+
|
|
136
|
+
# 2) Agentic compiler (optional; may require extra deps like google.genai)
|
|
137
|
+
try:
|
|
138
|
+
mod = importlib.import_module("syntaxmatrix.agentic.agents")
|
|
139
|
+
fn = getattr(mod, "compile_plan_to_html", None)
|
|
140
|
+
if callable(fn):
|
|
141
|
+
return fn(layout), "agentic.compile_plan_to_html"
|
|
142
|
+
errors.append("agentic: compile_plan_to_html not found/callable")
|
|
143
|
+
except Exception:
|
|
144
|
+
errors.append("agentic import failed:\n" + traceback.format_exc())
|
|
145
|
+
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"Could not import a compiler.\n\n" + "\n\n".join(errors)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _issues_to_text(issues) -> str:
|
|
151
|
+
lines = []
|
|
152
|
+
for it in issues:
|
|
153
|
+
d = it.to_dict() if hasattr(it, "to_dict") else dict(it)
|
|
154
|
+
lines.append(f"- [{d.get('level')}] {d.get('path')}: {d.get('message')}")
|
|
155
|
+
return "\n".join(lines)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def run_page_template_selftest(
|
|
159
|
+
fixture_path: Optional[str] = None,
|
|
160
|
+
*,
|
|
161
|
+
verbose: bool = True,
|
|
162
|
+
) -> int:
|
|
163
|
+
"""
|
|
164
|
+
Self-test:
|
|
165
|
+
1) Load fixture JSON (or use built-in About v1 fixture)
|
|
166
|
+
2) normalise + validate layout
|
|
167
|
+
3) compile HTML
|
|
168
|
+
4) validate compiled HTML anchors
|
|
169
|
+
5) patch publish with a modified layout
|
|
170
|
+
6) assert that hero + section titles/intros + grid items changed
|
|
171
|
+
|
|
172
|
+
Returns 0 if pass, non-zero if fail.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
# ── 1) Load fixture
|
|
176
|
+
if fixture_path:
|
|
177
|
+
raw = json.loads(Path(fixture_path).read_text(encoding="utf-8"))
|
|
178
|
+
else:
|
|
179
|
+
raw = _default_fixture_about_v1()
|
|
180
|
+
|
|
181
|
+
# ── 2) Normalise + validate layout
|
|
182
|
+
layout = normalise_layout(raw, mode="draft")
|
|
183
|
+
issues = validate_layout(layout)
|
|
184
|
+
errors = [i for i in issues if i.level == "error"]
|
|
185
|
+
warns = [i for i in issues if i.level == "warning"]
|
|
186
|
+
|
|
187
|
+
if verbose and warns:
|
|
188
|
+
print("Layout warnings:")
|
|
189
|
+
print(_issues_to_text(warns))
|
|
190
|
+
|
|
191
|
+
if errors:
|
|
192
|
+
print("Layout errors:")
|
|
193
|
+
print(_issues_to_text(errors))
|
|
194
|
+
return 2
|
|
195
|
+
|
|
196
|
+
# ── 3) Compile HTML
|
|
197
|
+
html, compiler_name = _compile_html(layout)
|
|
198
|
+
if verbose:
|
|
199
|
+
print(f"Compiled using: {compiler_name}")
|
|
200
|
+
print(f"HTML length: {len(html)} chars")
|
|
201
|
+
|
|
202
|
+
# ── 4) Validate compiled HTML anchors
|
|
203
|
+
html_issues = validate_compiled_html(html, layout)
|
|
204
|
+
html_errors = [i for i in html_issues if i.level == "error"]
|
|
205
|
+
html_warns = [i for i in html_issues if i.level == "warning"]
|
|
206
|
+
|
|
207
|
+
if verbose and html_warns:
|
|
208
|
+
print("HTML warnings:")
|
|
209
|
+
print(_issues_to_text(html_warns))
|
|
210
|
+
|
|
211
|
+
if html_errors:
|
|
212
|
+
print("HTML errors:")
|
|
213
|
+
print(_issues_to_text(html_errors))
|
|
214
|
+
return 3
|
|
215
|
+
|
|
216
|
+
# ── 5) Patch publish with a modified layout
|
|
217
|
+
mutated = copy.deepcopy(layout)
|
|
218
|
+
|
|
219
|
+
# Change hero fields
|
|
220
|
+
hero = next(s for s in mutated["sections"] if (s.get("type") or "").lower() == "hero")
|
|
221
|
+
old_hero_title = hero.get("title", "")
|
|
222
|
+
old_hero_text = hero.get("text", "")
|
|
223
|
+
old_hero_img = hero.get("imageUrl", "")
|
|
224
|
+
|
|
225
|
+
hero["title"] = "About SyntaxMatrix (Patched)"
|
|
226
|
+
hero["text"] = "This hero lead was patched successfully."
|
|
227
|
+
hero["imageUrl"] = "https://example.com/hero-b.jpg"
|
|
228
|
+
|
|
229
|
+
# ── 5) Patch publish with a modified layout
|
|
230
|
+
mutated = copy.deepcopy(layout)
|
|
231
|
+
|
|
232
|
+
# Change hero fields (always)
|
|
233
|
+
hero = next(s for s in mutated["sections"] if (s.get("type") or "").lower() == "hero")
|
|
234
|
+
old_hero_title = hero.get("title", "")
|
|
235
|
+
old_hero_text = hero.get("text", "")
|
|
236
|
+
old_hero_img = hero.get("imageUrl", "")
|
|
237
|
+
|
|
238
|
+
hero["title"] = "Hero (Patched)"
|
|
239
|
+
hero["text"] = "This hero lead was patched successfully."
|
|
240
|
+
hero["imageUrl"] = "https://example.com/hero-b.jpg"
|
|
241
|
+
|
|
242
|
+
# Pick a non-hero section to patch title + intro
|
|
243
|
+
target_sec = _pick_section_for_title_patch(mutated["sections"])
|
|
244
|
+
if target_sec is None:
|
|
245
|
+
raise RuntimeError("No non-hero section available to test section title/intro patching.")
|
|
246
|
+
|
|
247
|
+
target_id = target_sec.get("id")
|
|
248
|
+
old_target_title = target_sec.get("title", "")
|
|
249
|
+
old_target_text = target_sec.get("text", "")
|
|
250
|
+
|
|
251
|
+
target_sec["title"] = f"{old_target_title} (Patched)".strip() if old_target_title else "Section (Patched)"
|
|
252
|
+
target_sec["text"] = "This intro paragraph was patched successfully."
|
|
253
|
+
|
|
254
|
+
# Pick a grid section to patch first item
|
|
255
|
+
items_sec = _pick_section_for_item_patch(mutated["sections"])
|
|
256
|
+
if items_sec is None:
|
|
257
|
+
raise RuntimeError("No grid-like section with items found to test item patching.")
|
|
258
|
+
|
|
259
|
+
items_sec_id = items_sec.get("id")
|
|
260
|
+
items_list = items_sec.get("items") if isinstance(items_sec.get("items"), list) else []
|
|
261
|
+
old_item_title = (items_list[0].get("title", "") if items_list else "")
|
|
262
|
+
|
|
263
|
+
items_list[0]["title"] = f"{old_item_title} (Patched)".strip() if old_item_title else "Item (Patched)"
|
|
264
|
+
items_list[0]["text"] = "This card body was patched successfully."
|
|
265
|
+
|
|
266
|
+
patched_html, stats = patch_page_publish(html, mutated)
|
|
267
|
+
|
|
268
|
+
if verbose:
|
|
269
|
+
print("Patch stats:", stats)
|
|
270
|
+
|
|
271
|
+
patched_html, stats = patch_page_publish(html, mutated)
|
|
272
|
+
|
|
273
|
+
if verbose:
|
|
274
|
+
print("Patch stats:", stats)
|
|
275
|
+
|
|
276
|
+
# ── 6) Assertions: ensure old content removed, new content present
|
|
277
|
+
# ── 6) Assertions: verify the *target nodes* changed (not substring checks)
|
|
278
|
+
soup = BeautifulSoup(patched_html, "html.parser")
|
|
279
|
+
|
|
280
|
+
def text_of(selector: str) -> str:
|
|
281
|
+
el = soup.select_one(selector)
|
|
282
|
+
return el.get_text(strip=True) if el else ""
|
|
283
|
+
|
|
284
|
+
# Hero checks
|
|
285
|
+
hero_id = hero.get("id") or "sec_hero"
|
|
286
|
+
hero_h1 = text_of(f"section#{hero_id} h1")
|
|
287
|
+
if hero_h1 != "Hero (Patched)":
|
|
288
|
+
raise AssertionError(f"Hero <h1> not patched. Got: {hero_h1!r}")
|
|
289
|
+
|
|
290
|
+
hero_lead = (
|
|
291
|
+
text_of(f"section#{hero_id} p.lead")
|
|
292
|
+
or text_of(f"section#{hero_id} .lead")
|
|
293
|
+
or text_of(f"section#{hero_id} p")
|
|
294
|
+
)
|
|
295
|
+
if hero_lead != "This hero lead was patched successfully.":
|
|
296
|
+
raise AssertionError(f"Hero lead not patched. Got: {hero_lead!r}")
|
|
297
|
+
|
|
298
|
+
hero_bg = soup.select_one(f"section#{hero_id} .hero-bg")
|
|
299
|
+
if hero_bg is None:
|
|
300
|
+
raise AssertionError("Hero .hero-bg not found after patch.")
|
|
301
|
+
style = hero_bg.get("style") or ""
|
|
302
|
+
if "https://example.com/hero-b.jpg" not in style:
|
|
303
|
+
raise AssertionError(f"Hero bg not patched. style={style!r}")
|
|
304
|
+
|
|
305
|
+
# Section title + intro checks (dynamic)
|
|
306
|
+
sec_tag = soup.select_one(f"section#{target_id}")
|
|
307
|
+
if sec_tag is None:
|
|
308
|
+
raise AssertionError(f"Target section '{target_id}' not found after patch.")
|
|
309
|
+
h2 = sec_tag.find("h2")
|
|
310
|
+
if h2 is None:
|
|
311
|
+
raise AssertionError(f"Target section '{target_id}' has no <h2> after patch.")
|
|
312
|
+
if h2.get_text(strip=True) != target_sec["title"]:
|
|
313
|
+
raise AssertionError(f"Target section title not patched. Got: {h2.get_text(strip=True)!r}")
|
|
314
|
+
|
|
315
|
+
p_after = h2.find_next_sibling()
|
|
316
|
+
intro_text = p_after.get_text(strip=True) if (p_after is not None and p_after.name == "p") else ""
|
|
317
|
+
if intro_text != "This intro paragraph was patched successfully.":
|
|
318
|
+
# fallback: accept if the text exists somewhere in the section
|
|
319
|
+
if "This intro paragraph was patched successfully." not in sec_tag.get_text(" ", strip=True):
|
|
320
|
+
raise AssertionError(f"Target section intro not patched. Got: {intro_text!r}")
|
|
321
|
+
|
|
322
|
+
# Item patch checks (dynamic)
|
|
323
|
+
items_sec_tag = soup.select_one(f"section#{items_sec_id}")
|
|
324
|
+
if items_sec_tag is None:
|
|
325
|
+
raise AssertionError(f"Items section '{items_sec_id}' not found after patch.")
|
|
326
|
+
card_h3 = items_sec_tag.select_one(".card h3") or items_sec_tag.find("h3")
|
|
327
|
+
if card_h3 is None:
|
|
328
|
+
raise AssertionError(f"Items section '{items_sec_id}' has no item <h3> to assert.")
|
|
329
|
+
if card_h3.get_text(strip=True) != items_list[0]["title"]:
|
|
330
|
+
raise AssertionError(f"First item title not patched. Got: {card_h3.get_text(strip=True)!r}")
|
|
331
|
+
|
|
332
|
+
# Optional: re-validate anchors still OK after patch
|
|
333
|
+
post_issues = validate_compiled_html(patched_html, mutated)
|
|
334
|
+
post_errors = [i for i in post_issues if i.level == "error"]
|
|
335
|
+
if post_errors:
|
|
336
|
+
print("Post-patch HTML errors:")
|
|
337
|
+
print(_issues_to_text(post_errors))
|
|
338
|
+
return 4
|
|
339
|
+
|
|
340
|
+
if verbose:
|
|
341
|
+
print("✅ Self-test passed.")
|
|
342
|
+
return 0
|
|
343
|
+
|
|
344
|
+
except AssertionError as e:
|
|
345
|
+
print(f"❌ Self-test assertion failed: {e}")
|
|
346
|
+
return 10
|
|
347
|
+
except Exception as e:
|
|
348
|
+
print(f"❌ Self-test crashed: {type(e).__name__}: {e}")
|
|
349
|
+
return 11
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
import argparse
|
|
354
|
+
parser = argparse.ArgumentParser(description="SyntaxMatrix page template self-test")
|
|
355
|
+
parser.add_argument("--fixture", default=None, help="Path to fixture JSON (optional)")
|
|
356
|
+
parser.add_argument("--quiet", action="store_true", help="Less output")
|
|
357
|
+
args = parser.parse_args()
|
|
358
|
+
|
|
359
|
+
code = run_page_template_selftest(args.fixture, verbose=not args.quiet)
|
|
360
|
+
raise SystemExit(code)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
from syntaxmatrix.project_root import detect_project_root
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def getenv_api_key(client_dir, key_name):
|
|
7
|
+
_CLIENT_DOTENV_PATH = os.path.join(str(client_dir.parent), ".env")
|
|
8
|
+
if os.path.isfile(_CLIENT_DOTENV_PATH):
|
|
9
|
+
load_dotenv(_CLIENT_DOTENV_PATH, override=True)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
return os.getenv(key_name)
|
|
13
|
+
except Exception:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_client_file(client_dir, client_file):
|
|
18
|
+
client_file_path = os.path.join(str(client_dir.parent), client_file)
|
|
19
|
+
try:
|
|
20
|
+
with open(client_file_path, 'r') as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
return content
|
|
23
|
+
except Exception as e:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
# def client_media_path(client_dir, client_localassets_file):
|
|
27
|
+
# client_asset = os.path.join(str(client_dir.parent), client_localassets_file)
|
|
28
|
+
# return client_asset
|