TypeDAL 4.4.2__tar.gz → 4.4.4__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 (74) hide show
  1. {typedal-4.4.2 → typedal-4.4.4}/.crush/crush.db-shm +0 -0
  2. typedal-4.4.4/.crush/crush.db-wal +0 -0
  3. {typedal-4.4.2 → typedal-4.4.4}/.crush/logs/crush.log +9 -0
  4. {typedal-4.4.2 → typedal-4.4.4}/CHANGELOG.md +12 -0
  5. {typedal-4.4.2 → typedal-4.4.4}/PKG-INFO +3 -2
  6. {typedal-4.4.2 → typedal-4.4.4}/mkdocs.yml +2 -1
  7. {typedal-4.4.2 → typedal-4.4.4}/pyproject.toml +2 -1
  8. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/__about__.py +1 -1
  9. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/config.py +2 -84
  10. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/relationships.py +8 -1
  11. typedal-4.4.4/tasks.py +54 -0
  12. {typedal-4.4.2 → typedal-4.4.4}/tests/test_relationships.py +4 -1
  13. typedal-4.4.2/.crush/crush.db-wal +0 -0
  14. {typedal-4.4.2 → typedal-4.4.4}/.crush/.gitignore +0 -0
  15. {typedal-4.4.2 → typedal-4.4.4}/.crush/init +0 -0
  16. {typedal-4.4.2 → typedal-4.4.4}/.github/workflows/su6.yml +0 -0
  17. {typedal-4.4.2 → typedal-4.4.4}/.gitignore +0 -0
  18. {typedal-4.4.2 → typedal-4.4.4}/.readthedocs.yml +0 -0
  19. {typedal-4.4.2 → typedal-4.4.4}/README.md +0 -0
  20. {typedal-4.4.2 → typedal-4.4.4}/coverage.svg +0 -0
  21. {typedal-4.4.2 → typedal-4.4.4}/docs/1_getting_started.md +0 -0
  22. {typedal-4.4.2 → typedal-4.4.4}/docs/2_defining_tables.md +0 -0
  23. {typedal-4.4.2 → typedal-4.4.4}/docs/3_building_queries.md +0 -0
  24. {typedal-4.4.2 → typedal-4.4.4}/docs/4_relationships.md +0 -0
  25. {typedal-4.4.2 → typedal-4.4.4}/docs/5_py4web.md +0 -0
  26. {typedal-4.4.2 → typedal-4.4.4}/docs/6_migrations.md +0 -0
  27. {typedal-4.4.2 → typedal-4.4.4}/docs/7_configuration.md +0 -0
  28. {typedal-4.4.2 → typedal-4.4.4}/docs/8_mixins.md +0 -0
  29. {typedal-4.4.2 → typedal-4.4.4}/docs/9_memoization.md +0 -0
  30. {typedal-4.4.2 → typedal-4.4.4}/docs/css/code_blocks.css +0 -0
  31. {typedal-4.4.2 → typedal-4.4.4}/docs/index.md +0 -0
  32. {typedal-4.4.2 → typedal-4.4.4}/docs/requirements.txt +0 -0
  33. {typedal-4.4.2 → typedal-4.4.4}/example_new.py +0 -0
  34. {typedal-4.4.2 → typedal-4.4.4}/example_old.py +0 -0
  35. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/__init__.py +0 -0
  36. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/caching.py +0 -0
  37. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/cli.py +0 -0
  38. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/constants.py +0 -0
  39. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/core.py +0 -0
  40. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/define.py +0 -0
  41. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/fields.py +0 -0
  42. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/for_py4web.py +0 -0
  43. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/for_web2py.py +0 -0
  44. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/helpers.py +0 -0
  45. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/mixins.py +0 -0
  46. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/py.typed +0 -0
  47. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/query_builder.py +0 -0
  48. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/rows.py +0 -0
  49. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/serializers/as_json.py +0 -0
  50. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/tables.py +0 -0
  51. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/types.py +0 -0
  52. {typedal-4.4.2 → typedal-4.4.4}/src/typedal/web2py_py4web_shared.py +0 -0
  53. {typedal-4.4.2 → typedal-4.4.4}/tests/__init__.py +0 -0
  54. {typedal-4.4.2 → typedal-4.4.4}/tests/configs/simple.toml +0 -0
  55. {typedal-4.4.2 → typedal-4.4.4}/tests/configs/valid.env +0 -0
  56. {typedal-4.4.2 → typedal-4.4.4}/tests/configs/valid.toml +0 -0
  57. {typedal-4.4.2 → typedal-4.4.4}/tests/py314_tests.py +0 -0
  58. {typedal-4.4.2 → typedal-4.4.4}/tests/test_cli.py +0 -0
  59. {typedal-4.4.2 → typedal-4.4.4}/tests/test_config.py +0 -0
  60. {typedal-4.4.2 → typedal-4.4.4}/tests/test_docs_examples.py +0 -0
  61. {typedal-4.4.2 → typedal-4.4.4}/tests/test_helpers.py +0 -0
  62. {typedal-4.4.2 → typedal-4.4.4}/tests/test_json.py +0 -0
  63. {typedal-4.4.2 → typedal-4.4.4}/tests/test_main.py +0 -0
  64. {typedal-4.4.2 → typedal-4.4.4}/tests/test_mixins.py +0 -0
  65. {typedal-4.4.2 → typedal-4.4.4}/tests/test_mypy.py +0 -0
  66. {typedal-4.4.2 → typedal-4.4.4}/tests/test_orm.py +0 -0
  67. {typedal-4.4.2 → typedal-4.4.4}/tests/test_py4web.py +0 -0
  68. {typedal-4.4.2 → typedal-4.4.4}/tests/test_query_builder.py +0 -0
  69. {typedal-4.4.2 → typedal-4.4.4}/tests/test_row.py +0 -0
  70. {typedal-4.4.2 → typedal-4.4.4}/tests/test_stats.py +0 -0
  71. {typedal-4.4.2 → typedal-4.4.4}/tests/test_table.py +0 -0
  72. {typedal-4.4.2 → typedal-4.4.4}/tests/test_web2py.py +0 -0
  73. {typedal-4.4.2 → typedal-4.4.4}/tests/test_xx_others.py +0 -0
  74. {typedal-4.4.2 → typedal-4.4.4}/tests/timings.py +0 -0
Binary file
@@ -23,3 +23,12 @@
23
23
  {"time":"2026-01-26T17:11:34.786142525+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).createAndStartLSPClient","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":76},"msg":"LSP client initialized","name":"python"}
24
24
  {"time":"2026-01-26T17:11:43.042930337+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
25
25
  {"time":"2026-01-26T17:11:44.911909276+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
26
+ {"time":"2026-01-26T18:10:19.727802782+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":55},"msg":"Fetching providers from Catwalk"}
27
+ {"time":"2026-01-26T18:10:19.926137244+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/config.(*catwalkSync).Get.func1","file":"github.com/charmbracelet/crush/internal/config/catwalk.go","line":63},"msg":"Catwalk providers not modified"}
28
+ {"time":"2026-01-26T18:10:19.926245207+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/config.(*Config).configureProviders","file":"github.com/charmbracelet/crush/internal/config/load.go","line":259},"msg":"Skipping provider due to missing API key","provider":"anthropic"}
29
+ {"time":"2026-01-26T18:10:19.927808467+01:00","level":"INFO","msg":"goose: no migrations to run. current version: 20250812000000"}
30
+ {"time":"2026-01-26T18:10:19.927858435+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).initLSPClients","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":21},"msg":"LSP clients initialization started in background"}
31
+ {"time":"2026-01-26T18:10:19.928006013+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.New.func1","file":"github.com/charmbracelet/crush/internal/app/app.go","line":110},"msg":"Initializing MCP clients"}
32
+ {"time":"2026-01-26T18:10:20.546676356+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/app.(*App).createAndStartLSPClient","file":"github.com/charmbracelet/crush/internal/app/lsp.go","line":76},"msg":"LSP client initialized","name":"python"}
33
+ {"time":"2026-01-26T18:11:07.640056766+01:00","level":"INFO","source":{"function":"github.com/charmbracelet/crush/internal/agent.(*sessionAgent).generateTitle","file":"github.com/charmbracelet/crush/internal/agent/agent.go","line":788},"msg":"generated title with small model"}
34
+ {"time":"2026-01-26T18:11:11.201161834+01:00","level":"WARN","source":{"function":"github.com/charmbracelet/crush/internal/agent/tools.init.func1","file":"github.com/charmbracelet/crush/internal/agent/tools/rg.go","line":18},"msg":"Ripgrep (rg) not found in $PATH. Some grep features might be limited or slower."}
@@ -2,6 +2,18 @@
2
2
 
3
3
  <!--next-version-placeholder-->
4
4
 
5
+ ## v4.4.4 (2026-02-25)
6
+
7
+ ### Fix
8
+
9
+ * **relationship:** Ensure get_db() also works with forward references by looking at other table (owner) as fallback ([`17dd7b4`](https://github.com/trialandsuccess/TypeDAL/commit/17dd7b4273819f2edf6d427264ee1a81622ef597))
10
+
11
+ ## v4.4.3 (2026-02-11)
12
+
13
+ ### Fix
14
+
15
+ * Move env shell-style expansion to configuraptor, improved functionality using expandvars instead of custom implementation ([`8b41b91`](https://github.com/trialandsuccess/TypeDAL/commit/8b41b91d55c8893d5ce15c2decf6daa855fa230b))
16
+
5
17
  ## v4.4.2 (2026-01-26)
6
18
 
7
19
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TypeDAL
3
- Version: 4.4.2
3
+ Version: 4.4.4
4
4
  Summary: Typing support for PyDAL
5
5
  Project-URL: Documentation, https://typedal.readthedocs.io/
6
6
  Project-URL: Issues, https://github.com/trialandsuccess/TypeDAL/issues
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
16
16
  Classifier: Programming Language :: Python :: Implementation :: PyPy
17
17
  Requires-Python: >=3.11
18
18
  Requires-Dist: configurable-json<2
19
- Requires-Dist: configuraptor<2,>=1.27.1
19
+ Requires-Dist: configuraptor<3,>=2.0.1
20
20
  Requires-Dist: dill<1
21
21
  Requires-Dist: legacy-cgi; python_version >= '3.13'
22
22
  Requires-Dist: pydal>=20251012.3
@@ -32,6 +32,7 @@ Requires-Dist: tomlkit; extra == 'all'
32
32
  Requires-Dist: typer<0.19,>=0.18; extra == 'all'
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: contextlib-chdir; extra == 'dev'
35
+ Requires-Dist: ewok; extra == 'dev'
35
36
  Requires-Dist: hatch; extra == 'dev'
36
37
  Requires-Dist: mkdocs; extra == 'dev'
37
38
  Requires-Dist: mkdocs-dracula-theme; extra == 'dev'
@@ -6,10 +6,11 @@ nav:
6
6
  - 2. Defining Tables: 2_defining_tables.md
7
7
  - 3. Building Queries: 3_building_queries.md
8
8
  - 4. Relationships: 4_relationships.md
9
- - 5. py4web: 5_py4web.md
9
+ - 5. py4web and web2py: 5_py4web.md
10
10
  - 6. Migrations: 6_migrations.md
11
11
  - 7. Configuration: 7_configuration.md
12
12
  - 8. Mixins: 8_mixins.md
13
+ - 9. Function Memoization: 9_memoization.md
13
14
  extra:
14
15
  version:
15
16
  default: stable
@@ -29,7 +29,7 @@ classifiers = [
29
29
  dependencies = [
30
30
  "pydal >= 20251012.3", # core
31
31
  "dill < 1", # caching
32
- "configuraptor >= 1.27.1, < 2", # config
32
+ "configuraptor >= 2.0.1, < 3", # config
33
33
  "Configurable-JSON < 2", # json dumping
34
34
  "python-slugify < 9",
35
35
  "legacy-cgi; python_version >= '3.13'",
@@ -63,6 +63,7 @@ all = [
63
63
  dev = [
64
64
  # build:
65
65
  "hatch",
66
+ "ewok",
66
67
  # test:
67
68
  "su6[all]>=1.9.0",
68
69
  "python-semantic-release < 8",
@@ -5,4 +5,4 @@ This file contains the Version info for this package.
5
5
  # SPDX-FileCopyrightText: 2023-present Robin van der Noord <robinvandernoord@gmail.com>
6
6
  #
7
7
  # SPDX-License-Identifier: MIT
8
- __version__ = "4.4.2"
8
+ __version__ = "4.4.4"
@@ -3,10 +3,8 @@ TypeDAL can be configured by a combination of pyproject.toml (static), env (dyna
3
3
  """
4
4
 
5
5
  import os
6
- import re
7
6
  import typing as t
8
7
  import warnings
9
- from collections import defaultdict
10
8
  from pathlib import Path
11
9
 
12
10
  import tomli
@@ -16,6 +14,8 @@ from dotenv import dotenv_values, find_dotenv
16
14
 
17
15
  from .types import AnyDict
18
16
 
17
+ from configuraptor.helpers import expand_env_vars_into_toml_values
18
+
19
19
  if t.TYPE_CHECKING:
20
20
  from edwh_migrate import Config as MigrateConfig
21
21
  from pydal2sql.typer_support import Config as P2SConfig
@@ -244,88 +244,6 @@ def transform(data: AnyDict, prop: str) -> bool:
244
244
  return False
245
245
 
246
246
 
247
- def expand_posix_vars(posix_expr: str, context: dict[str, str]) -> str:
248
- """
249
- Replace case-insensitive POSIX and Docker Compose-like environment variables in a string with their values.
250
-
251
- Args:
252
- posix_expr (str): The input string containing case-insensitive POSIX or Docker Compose-like variables.
253
- context (dict): A dictionary containing variable names and their respective values.
254
-
255
- Returns:
256
- str: The string with replaced variable values.
257
-
258
- See Also:
259
- https://stackoverflow.com/questions/386934/how-to-evaluate-environment-variables-into-a-string-in-python
260
- and ChatGPT
261
- """
262
- env = defaultdict(lambda: "")
263
- for key, value in context.items():
264
- env[key.lower()] = value
265
-
266
- # Regular expression to match "${VAR:default}" pattern
267
- pattern = r"\$\{([^}]+)\}"
268
-
269
- def replace_var(match: re.Match[t.Any]) -> str:
270
- var_with_default = match.group(1)
271
- var_name, default_value = var_with_default.split(":") if ":" in var_with_default else (var_with_default, "")
272
- return env.get(var_name.lower(), default_value)
273
-
274
- return re.sub(pattern, replace_var, posix_expr)
275
-
276
-
277
- def expand_env_vars_into_toml_values(toml: AnyDict, env: AnyDict) -> None:
278
- """
279
- Recursively expands POSIX/Docker Compose-like environment variables in a TOML dictionary.
280
-
281
- This function traverses a TOML dictionary and expands POSIX/Docker Compose-like
282
- environment variables (${VAR:default}) using values provided in the 'env' dictionary.
283
- It performs in-place modification of the 'toml' dictionary.
284
-
285
- Args:
286
- toml (dict): A TOML dictionary with string values possibly containing environment variables.
287
- env (dict): A dictionary containing environment variable names and their respective values.
288
-
289
- Returns:
290
- None: The function modifies the 'toml' dictionary in place.
291
-
292
- Notes:
293
- The function recursively traverses the 'toml' dictionary. If a value is a string or a list of strings,
294
- it attempts to substitute any environment variables found within those strings using the 'env' dictionary.
295
-
296
- Example:
297
- toml_data = {
298
- 'key1': 'This has ${ENV_VAR:default}',
299
- 'key2': ['String with ${ANOTHER_VAR}', 'Another ${YET_ANOTHER_VAR}']
300
- }
301
- environment = {
302
- 'ENV_VAR': 'replaced_value',
303
- 'ANOTHER_VAR': 'value_1',
304
- 'YET_ANOTHER_VAR': 'value_2'
305
- }
306
-
307
- expand_env_vars_into_toml_values(toml_data, environment)
308
- # 'toml_data' will be modified in place:
309
- # {
310
- # 'key1': 'This has replaced_value',
311
- # 'key2': ['String with value_1', 'Another value_2']
312
- # }
313
- """
314
- if not toml or not env:
315
- return
316
-
317
- for key, var in toml.items():
318
- if isinstance(var, dict):
319
- expand_env_vars_into_toml_values(var, env)
320
- elif isinstance(var, list):
321
- toml[key] = [expand_posix_vars(_, env) for _ in var if isinstance(_, str)]
322
- elif isinstance(var, str):
323
- toml[key] = expand_posix_vars(var, env)
324
- else:
325
- # nothing to substitute
326
- continue
327
-
328
-
329
247
  def load_config(
330
248
  connection_name: t.Optional[str] = None,
331
249
  _use_pyproject: bool | str | None = True,
@@ -37,6 +37,7 @@ class Relationship(t.Generic[To_Type]):
37
37
  nested: dict[str, t.Self]
38
38
  explicit: bool
39
39
  name: str | None = None # set by __set_name__
40
+ _owner: t.Type["TypedTable"] | None = None
40
41
 
41
42
  def __init__(
42
43
  self,
@@ -120,6 +121,7 @@ class Relationship(t.Generic[To_Type]):
120
121
 
121
122
  def __set_name__(self, owner: t.Type["TypedTable"], name: str) -> None:
122
123
  """Called automatically when assigned to a class attribute."""
124
+ self._owner = owner
123
125
  self.name = name
124
126
 
125
127
  def get_table(self, db: "TypeDAL") -> t.Type["TypedTable"]:
@@ -145,7 +147,12 @@ class Relationship(t.Generic[To_Type]):
145
147
  Returns:
146
148
  TypeDAL | None: The database instance if it exists, or None otherwise.
147
149
  """
148
- return getattr(self.table, "_db", None)
150
+ target = self.table
151
+
152
+ if isinstance(target, str) and (owner := self._owner):
153
+ target = owner
154
+
155
+ return getattr(target, "_db", None)
149
156
 
150
157
  @property
151
158
  def lazy(self) -> LazyPolicy:
typedal-4.4.4/tasks.py ADDED
@@ -0,0 +1,54 @@
1
+ """Task automation using ewok (invoke-compatible)."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from ewok import Context, task
7
+
8
+ # Compiled regex pattern for replacing the nav section in mkdocs.yml
9
+ NAV_SECTION_PATTERN = re.compile(r"nav:.*?(?=\n[a-z_]+:|$)", re.DOTALL)
10
+
11
+
12
+ def extract_title(md_file: Path) -> str:
13
+ """Extract the title from a markdown file's first heading."""
14
+ first_line = md_file.read_text(encoding="utf-8").split("\n", 1)[0].strip()
15
+ # Remove the leading # and any extra whitespace
16
+ return first_line.lstrip("#").strip()
17
+
18
+
19
+ def generate_nav_entries() -> list[str]:
20
+ """Generate nav entries from numbered markdown files in docs/."""
21
+ docs_dir = Path(__file__).parent / "docs"
22
+
23
+ # Find all numbered chapter files (supports 1-N digits)
24
+ chapters = [f for f in docs_dir.glob("*_*.md") if f.stem.split("_", 1)[0].isdigit()]
25
+
26
+ return [
27
+ f" - {extract_title(chapter)}: {chapter.name}"
28
+ for chapter in sorted(
29
+ chapters,
30
+ key=lambda f: int(f.stem.split("_", 1)[0]),
31
+ )
32
+ ]
33
+
34
+
35
+ @task
36
+ def update_docs_nav(ctx: Context) -> None:
37
+ """Update mkdocs.yml nav section from actual markdown files to prevent sync issues."""
38
+ mkdocs_file = Path(__file__).parent / "mkdocs.yml"
39
+
40
+ content = mkdocs_file.read_text(encoding="utf-8")
41
+
42
+ # Generate new nav entries
43
+ nav_entries = generate_nav_entries()
44
+ new_nav_section = "nav:\n" + "\n".join(nav_entries)
45
+
46
+ # Replace the nav section
47
+ updated_content = NAV_SECTION_PATTERN.sub(new_nav_section, content)
48
+
49
+ mkdocs_file.write_text(updated_content, encoding="utf-8")
50
+
51
+ print(f"✓ Updated mkdocs.yml with {len(nav_entries)} chapters")
52
+ print("\nGenerated nav:")
53
+ for entry in nav_entries:
54
+ print(entry)
@@ -19,7 +19,7 @@ from src.typedal.caching import (
19
19
  from src.typedal.serializers import as_json
20
20
  from typedal import TypedRows
21
21
 
22
- db = TypeDAL("sqlite:memory")
22
+ db = TypeDAL("sqlite:memory", lazy_policy="warn")
23
23
 
24
24
 
25
25
  class TaggableMixin:
@@ -336,6 +336,7 @@ def test_reprs():
336
336
  empty = Relationship("article")
337
337
 
338
338
  assert empty.get_table(db) == Article
339
+ assert empty.get_db() is None
339
340
 
340
341
  db.define_table("new")
341
342
  empty = Relationship("new")
@@ -345,8 +346,10 @@ def test_reprs():
345
346
 
346
347
  empty = Relationship(db.new)
347
348
  assert empty.get_table(db) == db.new
349
+ assert empty.get_db() == db
348
350
 
349
351
  assert empty.get_table_name() == "new"
352
+ assert User.bestie.get_db() == db
350
353
 
351
354
  relation = Article.join("author").relationships["author"]
352
355
 
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes