dars-framework 1.2.3__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.
Files changed (118) hide show
  1. dars/__init__.py +0 -0
  2. dars/all.py +69 -0
  3. dars/cli/__init__.py +0 -0
  4. dars/cli/doctor/__init__.py +1 -0
  5. dars/cli/doctor/detect.py +154 -0
  6. dars/cli/doctor/doctor.py +176 -0
  7. dars/cli/doctor/installers.py +100 -0
  8. dars/cli/doctor/persist.py +62 -0
  9. dars/cli/doctor/preflight.py +33 -0
  10. dars/cli/doctor/ui.py +54 -0
  11. dars/cli/hot_reload.py +33 -0
  12. dars/cli/main.py +1107 -0
  13. dars/cli/preview.py +448 -0
  14. dars/cli/translations.py +531 -0
  15. dars/components/__init__.py +0 -0
  16. dars/components/advanced/__init__.py +8 -0
  17. dars/components/advanced/accordion.py +26 -0
  18. dars/components/advanced/card.py +33 -0
  19. dars/components/advanced/modal.py +45 -0
  20. dars/components/advanced/navbar.py +44 -0
  21. dars/components/advanced/table.py +25 -0
  22. dars/components/advanced/tabs.py +31 -0
  23. dars/components/basic/__init__.py +34 -0
  24. dars/components/basic/button.py +55 -0
  25. dars/components/basic/checkbox.py +35 -0
  26. dars/components/basic/container.py +29 -0
  27. dars/components/basic/datepicker.py +139 -0
  28. dars/components/basic/image.py +36 -0
  29. dars/components/basic/input.py +57 -0
  30. dars/components/basic/link.py +31 -0
  31. dars/components/basic/markdown.py +86 -0
  32. dars/components/basic/page.py +20 -0
  33. dars/components/basic/progressbar.py +18 -0
  34. dars/components/basic/radiobutton.py +35 -0
  35. dars/components/basic/select.py +82 -0
  36. dars/components/basic/slider.py +63 -0
  37. dars/components/basic/spinner.py +12 -0
  38. dars/components/basic/text.py +23 -0
  39. dars/components/basic/textarea.py +46 -0
  40. dars/components/basic/tooltip.py +19 -0
  41. dars/components/layout/__init__.py +0 -0
  42. dars/components/layout/anchor.py +13 -0
  43. dars/components/layout/flex.py +26 -0
  44. dars/components/layout/grid.py +45 -0
  45. dars/config.py +134 -0
  46. dars/core/__init__.py +0 -0
  47. dars/core/app.py +957 -0
  48. dars/core/component.py +284 -0
  49. dars/core/events.py +102 -0
  50. dars/core/js_bridge.py +99 -0
  51. dars/core/properties.py +127 -0
  52. dars/core/state.py +309 -0
  53. dars/dars_tests/apps_test/health_check.py +56 -0
  54. dars/dars_tests/run_tests.py +275 -0
  55. dars/dars_tests/tests/test_advanced_components.py +69 -0
  56. dars/dars_tests/tests/test_basic_components.py +88 -0
  57. dars/dars_tests/tests/test_core_and_cli.py +17 -0
  58. dars/dars_tests/tests/test_layout_components.py +58 -0
  59. dars/dars_tests/tests/test_version_check.py +21 -0
  60. dars/docs/__init__.py +0 -0
  61. dars/docs/app.md +290 -0
  62. dars/docs/cli.md +80 -0
  63. dars/docs/components.md +1679 -0
  64. dars/docs/custom_components.md +30 -0
  65. dars/docs/events.md +45 -0
  66. dars/docs/exporters.md +162 -0
  67. dars/docs/getting_started.md +79 -0
  68. dars/docs/index.md +18 -0
  69. dars/docs/scripts.md +593 -0
  70. dars/docs/state_management.md +57 -0
  71. dars/exporters/__init__.py +0 -0
  72. dars/exporters/base.py +96 -0
  73. dars/exporters/web/OLD/html_css_js_OLD4.py +1538 -0
  74. dars/exporters/web/OLD/html_css_js_old.py +1406 -0
  75. dars/exporters/web/OLD/html_css_js_old2.py +1406 -0
  76. dars/exporters/web/__init__.py +0 -0
  77. dars/exporters/web/html_css_js.py +2675 -0
  78. dars/exporters/web/vdom.py +251 -0
  79. dars/js_lib.py +206 -0
  80. dars/scripts/__init__.py +0 -0
  81. dars/scripts/dscript.py +26 -0
  82. dars/scripts/script.py +39 -0
  83. dars/security.py +195 -0
  84. dars/templates/__init__.py +0 -0
  85. dars/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  86. dars/templates/examples/README.md +4 -0
  87. dars/templates/examples/__pycache__/dynamic_event_demo.cpython-311.pyc +0 -0
  88. dars/templates/examples/advanced/Modal_Demo/advanced_modal_demo.py +275 -0
  89. dars/templates/examples/advanced/SimpleDashboard/dashboard.py +437 -0
  90. dars/templates/examples/advanced/SimpleModermWeb/modern_web_app.py +452 -0
  91. dars/templates/examples/advanced/VariousComponents/all_components_demo.py +87 -0
  92. dars/templates/examples/advanced/__init__.py +0 -0
  93. dars/templates/examples/advanced/dState/state_mods_demo.py +68 -0
  94. dars/templates/examples/basic/Forms/form_components.py +516 -0
  95. dars/templates/examples/basic/Forms/simple_form.py +379 -0
  96. dars/templates/examples/basic/HelloWorld/hello_world.py +56 -0
  97. dars/templates/examples/basic/Layouts/flex_layout_responsive.py +13 -0
  98. dars/templates/examples/basic/Layouts/grid_layout_responsive.py +12 -0
  99. dars/templates/examples/basic/Layouts/layout_multipage_demo.py +23 -0
  100. dars/templates/examples/basic/Multipage/multipage_example.py +67 -0
  101. dars/templates/examples/basic/PWA/icon-192x192.png +0 -0
  102. dars/templates/examples/basic/PWA/icon-512x512.png +0 -0
  103. dars/templates/examples/basic/PWA/pwa_custom_icons.py +33 -0
  104. dars/templates/examples/basic/__init__.py +0 -0
  105. dars/templates/examples/demo/__pycache__/complete_app.cpython-311.pyc +0 -0
  106. dars/templates/examples/demo/complete_app.py +21 -0
  107. dars/templates/examples/markdown/MarkdownTemplate/README.md +159 -0
  108. dars/templates/examples/markdown/MarkdownTemplate/markdown_template.py +21 -0
  109. dars/templates/examples/markdown/MarkdownTemplate/other_docs.md +1 -0
  110. dars/templates/examples/markdown/__init__.py +0 -0
  111. dars/templates/html/__init__.py +0 -0
  112. dars/version.py +2 -0
  113. dars_framework-1.2.3.dist-info/METADATA +15 -0
  114. dars_framework-1.2.3.dist-info/RECORD +118 -0
  115. dars_framework-1.2.3.dist-info/WHEEL +5 -0
  116. dars_framework-1.2.3.dist-info/entry_points.txt +2 -0
  117. dars_framework-1.2.3.dist-info/licenses/LICENSE +21 -0
  118. dars_framework-1.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,35 @@
1
+ from dars.core.component import Component
2
+ from dars.core.properties import StyleProps
3
+ from dars.core.events import EventTypes
4
+ from typing import Optional, Union, Dict, Any, Callable
5
+
6
+ class RadioButton(Component):
7
+ def __init__(
8
+ self,
9
+ label: str = "",
10
+ value: str = "",
11
+ name: str = "radio_group",
12
+ checked: bool = False,
13
+ id: Optional[str] = None,
14
+ class_name: Optional[str] = None,
15
+ style: Optional[Dict[str, Any]] = None,
16
+ disabled: bool = False,
17
+ required: bool = False,
18
+ on_change: Optional[Callable] = None,
19
+ **props
20
+ ):
21
+ super().__init__(id=id, class_name=class_name, style=style, **props)
22
+ self.label = label
23
+ self.value = value or label # Si no se proporciona value, usar label
24
+ self.name = name # Requerido para agrupar radio buttons
25
+ self.checked = checked
26
+ self.disabled = disabled
27
+ self.required = required
28
+
29
+ # Registrar evento de cambio si se proporciona
30
+ if on_change:
31
+ self.set_event(EventTypes.CHANGE, on_change)
32
+
33
+ def render(self, exporter: Any) -> str:
34
+ # El método render será implementado por cada exportador
35
+ raise NotImplementedError("El método render debe ser implementado por el exportador")
@@ -0,0 +1,82 @@
1
+ from dars.core.component import Component
2
+ from dars.core.properties import StyleProps
3
+ from dars.core.events import EventTypes
4
+ from typing import Optional, Union, Dict, Any, Callable, List
5
+
6
+ class SelectOption:
7
+ """Class to represent a select option"""
8
+ def __init__(self, value: str, label: str, disabled: bool = False):
9
+ self.value = value
10
+ self.label = label
11
+ self.disabled = disabled
12
+
13
+ class Select(Component):
14
+ def __init__(
15
+ self,
16
+ options: List[Union[SelectOption, Dict[str, Any], str]] = None,
17
+ value: Optional[str] = None,
18
+ placeholder: str = "Seleccionar...",
19
+ multiple: bool = False,
20
+ size: Optional[int] = None,
21
+ id: Optional[str] = None,
22
+ class_name: Optional[str] = None,
23
+ style: Optional[Dict[str, Any]] = None,
24
+ disabled: bool = False,
25
+ required: bool = False,
26
+ on_change: Optional[Callable] = None,
27
+ **props
28
+ ):
29
+ super().__init__(id=id, class_name=class_name, style=style, **props)
30
+ self.options = self._process_options(options or [])
31
+ self.value = value
32
+ self.placeholder = placeholder
33
+ self.multiple = multiple
34
+ self.size = size # Número de opciones visibles (para select múltiple)
35
+ self.disabled = disabled
36
+ self.required = required
37
+
38
+ # Registrar evento de cambio si se proporciona
39
+ if on_change:
40
+ self.set_event(EventTypes.CHANGE, on_change)
41
+
42
+ def _process_options(self, options: List[Union[SelectOption, Dict[str, Any], str]]) -> List[SelectOption]:
43
+ """Procesa las opciones y las convierte a objetos SelectOption"""
44
+ processed_options = []
45
+
46
+ for option in options:
47
+ if isinstance(option, SelectOption):
48
+ processed_options.append(option)
49
+ elif isinstance(option, dict):
50
+ processed_options.append(SelectOption(
51
+ value=option.get('value', ''),
52
+ label=option.get('label', option.get('value', '')),
53
+ disabled=option.get('disabled', False)
54
+ ))
55
+ elif isinstance(option, str):
56
+ processed_options.append(SelectOption(value=option, label=option))
57
+
58
+ return processed_options
59
+
60
+ def add_option(self, value: str, label: str = None, disabled: bool = False):
61
+ """Añade una nueva opción al select"""
62
+ self.options.append(SelectOption(
63
+ value=value,
64
+ label=label or value,
65
+ disabled=disabled
66
+ ))
67
+
68
+ def remove_option(self, value: str):
69
+ """Elimina una opción por su valor"""
70
+ self.options = [opt for opt in self.options if opt.value != value]
71
+
72
+ def get_selected_option(self) -> Optional[SelectOption]:
73
+ """Obtiene la opción seleccionada actualmente"""
74
+ if self.value:
75
+ for option in self.options:
76
+ if option.value == self.value:
77
+ return option
78
+ return None
79
+
80
+ def render(self, exporter: Any) -> str:
81
+ # El método render será implementado por cada exportador
82
+ raise NotImplementedError("El método render debe ser implementado por el exportador")
@@ -0,0 +1,63 @@
1
+ from dars.core.component import Component
2
+ from dars.core.properties import StyleProps
3
+ from dars.core.events import EventTypes
4
+ from typing import Optional, Union, Dict, Any, Callable
5
+
6
+ class Slider(Component):
7
+ def __init__(
8
+ self,
9
+ min_value: Union[int, float] = 0,
10
+ max_value: Union[int, float] = 100,
11
+ value: Union[int, float] = 50,
12
+ step: Union[int, float] = 1,
13
+ label: str = "",
14
+ show_value: bool = True,
15
+ orientation: str = "horizontal", # "horizontal" o "vertical"
16
+ id: Optional[str] = None,
17
+ class_name: Optional[str] = None,
18
+ style: Optional[Dict[str, Any]] = None,
19
+ disabled: bool = False,
20
+ on_change: Optional[Callable] = None,
21
+ on_input: Optional[Callable] = None
22
+ ):
23
+ super().__init__(id=id, class_name=class_name, style=style)
24
+ self.min_value = min_value
25
+ self.max_value = max_value
26
+ self.value = max(min_value, min(max_value, value)) # Asegurar que esté en rango
27
+ self.step = step
28
+ self.label = label
29
+ self.show_value = show_value
30
+ self.orientation = orientation
31
+ self.disabled = disabled
32
+
33
+ # Validar orientación
34
+ if orientation not in ["horizontal", "vertical"]:
35
+ raise ValueError("orientation debe ser 'horizontal' o 'vertical'")
36
+
37
+ # Registrar eventos si se proporcionan
38
+ if on_change:
39
+ self.set_event(EventTypes.CHANGE, on_change)
40
+ if on_input:
41
+ self.set_event(EventTypes.INPUT, on_input)
42
+
43
+ def set_value(self, value: Union[int, float]):
44
+ """Establece el valor del slider asegurando que esté en rango"""
45
+ self.value = max(self.min_value, min(self.max_value, value))
46
+
47
+ def get_percentage(self) -> float:
48
+ """Obtiene el porcentaje actual del slider (0-100)"""
49
+ if self.max_value == self.min_value:
50
+ return 0
51
+ return ((self.value - self.min_value) / (self.max_value - self.min_value)) * 100
52
+
53
+ def is_at_min(self) -> bool:
54
+ """Verifica si el slider está en su valor mínimo"""
55
+ return self.value == self.min_value
56
+
57
+ def is_at_max(self) -> bool:
58
+ """Verifica si el slider está en su valor máximo"""
59
+ return self.value == self.max_value
60
+
61
+ def render(self, exporter: Any) -> str:
62
+ # El método render será implementado por cada exportador
63
+ raise NotImplementedError("El método render debe ser implementado por el exportador")
@@ -0,0 +1,12 @@
1
+ from dars.core.component import Component
2
+
3
+ class Spinner(Component):
4
+ """
5
+ Circular loading indicator (spinner).
6
+ """
7
+
8
+ def __init__(self, **props):
9
+ super().__init__(**props)
10
+
11
+ def render(self) -> str:
12
+ return '<div class="dars-spinner"></div>'
@@ -0,0 +1,23 @@
1
+ from dars.core.component import Component
2
+ from dars.core.properties import StyleProps
3
+ from typing import Optional, Union, Dict, Any
4
+
5
+ class Text(Component):
6
+ def __init__(
7
+ self,
8
+ text: str = "",
9
+ id: Optional[str] = None,
10
+ class_name: Optional[str] = None,
11
+ style: Optional[Dict[str, Any]] = None,
12
+ **props
13
+ ):
14
+ super().__init__(id=id, class_name=class_name, style=style, **props)
15
+ self.text = text
16
+
17
+ def render(self, exporter: Any) -> str:
18
+ # El método render será implementado por cada exportador
19
+ # para generar el código específico de la plataforma.
20
+ # Por ahora, solo definimos la interfaz.
21
+ raise NotImplementedError("El método render debe ser implementado por el exportador")
22
+
23
+
@@ -0,0 +1,46 @@
1
+ from dars.core.component import Component
2
+ from typing import Optional, Dict, Any
3
+
4
+ class Textarea(Component):
5
+ """Component for multiline text areas."""
6
+ def __init__(
7
+ self,
8
+ value: str = "",
9
+ placeholder: str = "",
10
+ rows: int = 4,
11
+ cols: int = 50,
12
+ disabled: bool = False,
13
+ readonly: bool = False,
14
+ required: bool = False,
15
+ max_length: Optional[int] = None,
16
+ class_name: Optional[str] = None,
17
+ style: Optional[Dict[str, Any]] = None,
18
+ **kwargs
19
+ ):
20
+ super().__init__(class_name=class_name, style=style, **kwargs)
21
+ self.value = value
22
+ self.placeholder = placeholder
23
+ self.rows = rows
24
+ self.cols = cols
25
+ self.disabled = disabled
26
+ self.readonly = readonly
27
+ self.required = required
28
+ self.max_length = max_length
29
+
30
+ def render(self) -> str:
31
+ attrs = [
32
+ f'rows="{self.rows}"',
33
+ f'cols="{self.cols}"',
34
+ ]
35
+ if self.placeholder: attrs.append(f'placeholder="{self.placeholder}"')
36
+ if self.disabled: attrs.append('disabled')
37
+ if self.readonly: attrs.append('readonly')
38
+ if self.required: attrs.append('required')
39
+ if self.max_length: attrs.append(f'maxlength="{self.max_length}"')
40
+ if self.class_name: attrs.append(f'class="{self.class_name}"')
41
+ if self.style: attrs.append(f'style="{self.render_styles(self.style)}"')
42
+
43
+ return f'<textarea {" ".join(attrs)}>{self.value}</textarea>'
44
+
45
+
46
+
@@ -0,0 +1,19 @@
1
+ from dars.core.component import Component
2
+ from typing import Optional
3
+
4
+ class Tooltip(Component):
5
+ """
6
+ Tooltip: information box on hover.
7
+ text: text to display
8
+ child: wrapped component or HTML
9
+ position: top, right, bottom, left (optional)
10
+ """
11
+
12
+ def __init__(self, text: str, child: Component, position: Optional[str] = "top", **props):
13
+ super().__init__(**props)
14
+ self.text = text
15
+ self.child = child
16
+ self.position = position
17
+
18
+ def render(self) -> str:
19
+ return f'<div class="dars-tooltip dars-tooltip-{self.position}">{self.child.render() if hasattr(self.child, "render") else self.child}<span class="dars-tooltip-text">{self.text}</span></div>'
File without changes
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+
3
+ class AnchorPoint:
4
+ """
5
+ Represents an anchor or alignment point for a child in a layout (top, left, right, bottom, center, etc).
6
+ """
7
+ def __init__(self, x: Optional[str] = None, y: Optional[str] = None, name: Optional[str] = None):
8
+ self.x = x # e.g. 'left', 'center', 'right', percent or px
9
+ self.y = y # e.g. 'top', 'center', 'bottom', percent or px
10
+ self.name = name # Optional semantic name for anchor
11
+
12
+ def __repr__(self):
13
+ return f"AnchorPoint(x={self.x}, y={self.y}, name={self.name})"
@@ -0,0 +1,26 @@
1
+ from dars.components.layout.grid import LayoutBase
2
+ from typing import List, Optional
3
+
4
+ class FlexLayout(LayoutBase):
5
+ """
6
+ Responsive flexbox layout component. Allows direction, wrap, justify, align, and anchor points for children.
7
+ """
8
+ def __init__(self,
9
+ children: Optional[List[object]] = None,
10
+ direction: str = "row",
11
+ wrap: str = "wrap",
12
+ justify: str = "flex-start",
13
+ align: str = "stretch",
14
+ gap: str = "16px",
15
+ anchors: Optional[dict] = None,
16
+ **kwargs):
17
+ super().__init__(children=children, anchors=anchors, **kwargs)
18
+ self.direction = direction
19
+ self.wrap = wrap
20
+ self.justify = justify
21
+ self.align = align
22
+ self.gap = gap
23
+
24
+ def add_child(self, child, anchor: Optional[str] = None):
25
+ self.children.append(child)
26
+ # Could store anchor info per child if needed
@@ -0,0 +1,45 @@
1
+ from dars.core.component import Component
2
+ from typing import List, Optional, Dict, Any
3
+
4
+ class LayoutBase(Component):
5
+ """
6
+ Base class for all layout components. Allows adding children and anchor/positioning info.
7
+ """
8
+ def __init__(self, children: Optional[List[Component]] = None, anchors: Optional[Dict[str, Any]] = None, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.children = children or []
11
+ self.anchors = anchors or {}
12
+
13
+ def add_child(self, child: Component):
14
+ self.children.append(child)
15
+
16
+ def render(self, exporter=None):
17
+ # Layouts se renderizan solo por el exporter, pero se requiere para evitar TypeError
18
+ return ""
19
+
20
+ class GridLayout(LayoutBase):
21
+ """
22
+ Responsive grid layout component. Supports rows/columns and anchor points for children.
23
+ """
24
+ def __init__(self, rows: int = 1, cols: int = 1, children: Optional[List[Component]] = None, anchors: Optional[Dict[str, Any]] = None, gap: str = "16px", **kwargs):
25
+ super().__init__(children=children, anchors=anchors, **kwargs)
26
+ self.rows = rows
27
+ self.cols = cols
28
+ self.gap = gap
29
+
30
+ def add_child(self, child: Component, row: int = 0, col: int = 0, row_span: int = 1, col_span: int = 1, anchor: Optional[str] = None):
31
+ # Store child with layout info
32
+ if not hasattr(self, '_child_layout'):
33
+ self._child_layout = []
34
+ self._child_layout.append({
35
+ 'child': child,
36
+ 'row': row,
37
+ 'col': col,
38
+ 'row_span': row_span,
39
+ 'col_span': col_span,
40
+ 'anchor': anchor
41
+ })
42
+ self.children.append(child)
43
+
44
+ def get_child_layout(self):
45
+ return getattr(self, '_child_layout', [])
dars/config.py ADDED
@@ -0,0 +1,134 @@
1
+ import json
2
+ import os
3
+ from typing import Tuple, Dict, Any
4
+
5
+ DEFAULT_CONFIG = {
6
+ "entry": "main.py",
7
+ "format": "html",
8
+ "outdir": "dist",
9
+ "publicDir": None, # autodetect if None: prefers ./public then ./assets
10
+ "include": [],
11
+ "exclude": ["**/__pycache__", ".git", ".venv", "node_modules"],
12
+ "bundle": True,
13
+ }
14
+
15
+ CONFIG_FILENAME = "dars.config.json"
16
+
17
+
18
+ def load_config(project_root: str) -> Tuple[Dict[str, Any], bool]:
19
+ """Load dars.config.json from project_root. Returns (config, found)."""
20
+ config_path = os.path.join(project_root, CONFIG_FILENAME)
21
+ if not os.path.isfile(config_path):
22
+ # Autodetect public dir if exists for convenience even without config
23
+ cfg = DEFAULT_CONFIG.copy()
24
+ if os.path.isdir(os.path.join(project_root, "public")):
25
+ cfg["publicDir"] = "public"
26
+ elif os.path.isdir(os.path.join(project_root, "assets")):
27
+ cfg["publicDir"] = "assets"
28
+ return cfg, False
29
+ try:
30
+ with open(config_path, "r", encoding="utf-8") as f:
31
+ data = json.load(f)
32
+ cfg = DEFAULT_CONFIG.copy()
33
+ cfg.update({k: v for k, v in data.items() if v is not None})
34
+ return cfg, True
35
+ except Exception:
36
+ # On parse error, fallback to defaults but mark as not found to avoid enforcing
37
+ cfg = DEFAULT_CONFIG.copy()
38
+ return cfg, False
39
+
40
+
41
+ def resolve_paths(cfg: Dict[str, Any], project_root: str) -> Dict[str, Any]:
42
+ """Return a copy of cfg with absolute resolved paths for entry, publicDir, outdir."""
43
+ out = dict(cfg)
44
+ if out.get("entry"):
45
+ out["entry_abs"] = os.path.join(project_root, out["entry"])
46
+ else:
47
+ out["entry_abs"] = None
48
+ if out.get("publicDir"):
49
+ out["public_abs"] = os.path.join(project_root, out["publicDir"])
50
+ else:
51
+ # If None, keep None; exporter may still auto-detect
52
+ out["public_abs"] = None
53
+ if out.get("outdir"):
54
+ out["outdir_abs"] = os.path.join(project_root, out["outdir"])
55
+ else:
56
+ out["outdir_abs"] = os.path.join(project_root, "dist")
57
+ return out
58
+
59
+
60
+ def copy_public_dir(public_dir: str, dest_dir: str, include=None, exclude=None):
61
+ """Copy entire public_dir into dest_dir applying basic include/exclude filters.
62
+ Exclude supports substrings or glob-like simple matches; keep it simple to avoid heavy deps.
63
+ """
64
+ import shutil
65
+ from pathlib import Path
66
+
67
+ if not public_dir or not os.path.isdir(public_dir):
68
+ return
69
+
70
+ include = include or []
71
+ exclude = exclude or []
72
+
73
+ def is_excluded(path: Path) -> bool:
74
+ # Simple checks: substring or name matches
75
+ p_str = str(path)
76
+ for pat in exclude:
77
+ if pat in p_str:
78
+ return True
79
+ return False
80
+
81
+ def is_included(path: Path) -> bool:
82
+ if not include:
83
+ return True
84
+ p_str = str(path)
85
+ for pat in include:
86
+ if pat in p_str:
87
+ return True
88
+ return False
89
+
90
+ for root, dirs, files in os.walk(public_dir):
91
+ # filter excluded dirs in-place
92
+ dirs[:] = [d for d in dirs if not is_excluded(Path(root) / d)]
93
+ for f in files:
94
+ src_path = Path(root) / f
95
+ if is_excluded(src_path) or not is_included(src_path):
96
+ continue
97
+ rel = src_path.relative_to(public_dir)
98
+ dst_path = Path(dest_dir) / rel
99
+ os.makedirs(dst_path.parent, exist_ok=True)
100
+ try:
101
+ shutil.copy2(str(src_path), str(dst_path))
102
+ except Exception:
103
+ # Best-effort; ignore copy errors
104
+ pass
105
+
106
+
107
+ def write_default_config(project_root: str, overwrite: bool = False) -> str:
108
+ """Create a dars.config.json with defaults if it doesn't exist (or overwrite=True).
109
+ Returns the path to the config file.
110
+ """
111
+ path = os.path.join(project_root, CONFIG_FILENAME)
112
+ if os.path.exists(path) and not overwrite:
113
+ return path
114
+ cfg = DEFAULT_CONFIG.copy()
115
+ # Prefer autodetected public dir as default
116
+ if os.path.isdir(os.path.join(project_root, "public")):
117
+ cfg["publicDir"] = "public"
118
+ elif os.path.isdir(os.path.join(project_root, "assets")):
119
+ cfg["publicDir"] = "assets"
120
+ with open(path, "w", encoding="utf-8") as f:
121
+ json.dump(cfg, f, indent=2)
122
+ return path
123
+
124
+
125
+ def update_config(project_root: str, updates: Dict[str, Any]) -> str:
126
+ """Update or create config merging with defaults.
127
+ Returns the path to the config file.
128
+ """
129
+ cfg, _ = load_config(project_root)
130
+ cfg.update({k: v for k, v in updates.items() if v is not None})
131
+ path = os.path.join(project_root, CONFIG_FILENAME)
132
+ with open(path, "w", encoding="utf-8") as f:
133
+ json.dump(cfg, f, indent=2)
134
+ return path
dars/core/__init__.py ADDED
File without changes