libres 0.8.0__tar.gz → 0.9.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 (63) hide show
  1. {libres-0.8.0 → libres-0.9.0}/HISTORY.rst +15 -0
  2. {libres-0.8.0/src/libres.egg-info → libres-0.9.0}/PKG-INFO +20 -4
  3. {libres-0.8.0 → libres-0.9.0}/README.rst +2 -2
  4. {libres-0.8.0 → libres-0.9.0}/pyproject.toml +4 -6
  5. {libres-0.8.0 → libres-0.9.0}/src/libres/__init__.py +1 -1
  6. {libres-0.8.0 → libres-0.9.0}/src/libres/context/core.py +5 -5
  7. {libres-0.8.0 → libres-0.9.0}/src/libres/context/registry.py +1 -1
  8. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/allocation.py +14 -5
  9. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/reservation.py +3 -3
  10. libres-0.9.0/src/libres/db/models/types/json_type.py +46 -0
  11. {libres-0.8.0 → libres-0.9.0}/src/libres/db/scheduler.py +8 -4
  12. {libres-0.8.0 → libres-0.9.0/src/libres.egg-info}/PKG-INFO +20 -4
  13. {libres-0.8.0 → libres-0.9.0}/tests/test_scheduler.py +1 -1
  14. libres-0.8.0/src/libres/db/models/types/json_type.py +0 -51
  15. {libres-0.8.0 → libres-0.9.0}/LICENSE +0 -0
  16. {libres-0.8.0 → libres-0.9.0}/MANIFEST.in +0 -0
  17. {libres-0.8.0 → libres-0.9.0}/docs/Makefile +0 -0
  18. {libres-0.8.0 → libres-0.9.0}/docs/_static/custom.css +0 -0
  19. {libres-0.8.0 → libres-0.9.0}/docs/_static/favicon.ico +0 -0
  20. {libres-0.8.0 → libres-0.9.0}/docs/_static/logo.svg +0 -0
  21. {libres-0.8.0 → libres-0.9.0}/docs/api.rst +0 -0
  22. {libres-0.8.0 → libres-0.9.0}/docs/concepts.rst +0 -0
  23. {libres-0.8.0 → libres-0.9.0}/docs/conf.py +0 -0
  24. {libres-0.8.0 → libres-0.9.0}/docs/customizations.rst +0 -0
  25. {libres-0.8.0 → libres-0.9.0}/docs/faq.rst +0 -0
  26. {libres-0.8.0 → libres-0.9.0}/docs/index.rst +0 -0
  27. {libres-0.8.0 → libres-0.9.0}/docs/requirements.txt +0 -0
  28. {libres-0.8.0 → libres-0.9.0}/docs/under_the_hood.rst +0 -0
  29. {libres-0.8.0 → libres-0.9.0}/setup.cfg +0 -0
  30. {libres-0.8.0 → libres-0.9.0}/src/libres/.gitignore +0 -0
  31. {libres-0.8.0 → libres-0.9.0}/src/libres/context/__init__.py +0 -0
  32. {libres-0.8.0 → libres-0.9.0}/src/libres/context/exposure.py +0 -0
  33. {libres-0.8.0 → libres-0.9.0}/src/libres/context/session.py +0 -0
  34. {libres-0.8.0 → libres-0.9.0}/src/libres/context/settings.py +0 -0
  35. {libres-0.8.0 → libres-0.9.0}/src/libres/db/__init__.py +0 -0
  36. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/__init__.py +0 -0
  37. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/base.py +0 -0
  38. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/other.py +0 -0
  39. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/reserved_slot.py +0 -0
  40. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/timestamp.py +0 -0
  41. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/types/__init__.py +0 -0
  42. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/types/utcdatetime.py +0 -0
  43. {libres-0.8.0 → libres-0.9.0}/src/libres/db/models/types/uuid_type.py +0 -0
  44. {libres-0.8.0 → libres-0.9.0}/src/libres/db/queries.py +0 -0
  45. {libres-0.8.0 → libres-0.9.0}/src/libres/modules/__init__.py +0 -0
  46. {libres-0.8.0 → libres-0.9.0}/src/libres/modules/errors.py +0 -0
  47. {libres-0.8.0 → libres-0.9.0}/src/libres/modules/events.py +0 -0
  48. {libres-0.8.0 → libres-0.9.0}/src/libres/modules/rasterizer.py +0 -0
  49. {libres-0.8.0 → libres-0.9.0}/src/libres/modules/utils.py +0 -0
  50. {libres-0.8.0 → libres-0.9.0}/src/libres/py.typed +0 -0
  51. {libres-0.8.0 → libres-0.9.0}/src/libres.egg-info/SOURCES.txt +0 -0
  52. {libres-0.8.0 → libres-0.9.0}/src/libres.egg-info/dependency_links.txt +0 -0
  53. {libres-0.8.0 → libres-0.9.0}/src/libres.egg-info/not-zip-safe +0 -0
  54. {libres-0.8.0 → libres-0.9.0}/src/libres.egg-info/requires.txt +0 -0
  55. {libres-0.8.0 → libres-0.9.0}/src/libres.egg-info/top_level.txt +0 -0
  56. {libres-0.8.0 → libres-0.9.0}/tests/test_allocation.py +0 -0
  57. {libres-0.8.0 → libres-0.9.0}/tests/test_registry.py +0 -0
  58. {libres-0.8.0 → libres-0.9.0}/tests/test_reservation.py +0 -0
  59. {libres-0.8.0 → libres-0.9.0}/tests/test_reserved_slot.py +0 -0
  60. {libres-0.8.0 → libres-0.9.0}/tests/test_session.py +0 -0
  61. {libres-0.8.0 → libres-0.9.0}/tests/test_test.py +0 -0
  62. {libres-0.8.0 → libres-0.9.0}/tests/test_types.py +0 -0
  63. {libres-0.8.0 → libres-0.9.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,21 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 0.9.0 (23.05.2025)
5
+ ~~~~~~~~~~~~~~~~~~~
6
+
7
+ - Replaces `JSON` database type with `JSONB`, this means
8
+ Postgres as a backend is not required. You will also need
9
+ to write a migration for existing JSON columns. You may use
10
+ the following recipe using an alembic `Operations` object::
11
+
12
+ operations.alter_column(
13
+ 'table_name',
14
+ 'column_name',
15
+ type_=JSON,
16
+ postgresql_using='"column_name"::jsonb'
17
+ )
18
+
4
19
  0.8.0 (15.01.2025)
5
20
  ~~~~~~~~~~~~~~~~~~~
6
21
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: libres
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A library to reserve things
5
5
  Home-page: http://github.com/seantis/libres/
6
6
  Author: Denis Krienbühl
@@ -51,13 +51,14 @@ Requires-Dist: types-psycopg2; extra == "mypy"
51
51
  Requires-Dist: types-python-dateutil; extra == "mypy"
52
52
  Requires-Dist: types-pytz; extra == "mypy"
53
53
  Requires-Dist: typing-extensions; extra == "mypy"
54
+ Dynamic: license-file
54
55
 
55
56
  Libres
56
57
  ======
57
58
 
58
59
  Libres is a reservations management library to reserve things like tables at
59
- a restaurant or tickets at an event. It works with Python 3.8+
60
- and requires Postgresql 9.1+.
60
+ a restaurant or tickets at an event. It works with Python 3.9+
61
+ and requires Postgresql 9.2+.
61
62
 
62
63
  `Documentation <http://libres.readthedocs.org/en/latest/>`_ | `Source <http://github.com/seantis/libres/>`_ | `Bugs <http://github.com/seantis/libres/issues>`_
63
64
 
@@ -141,6 +142,21 @@ After this, create a new release on Github.
141
142
  Changelog
142
143
  ---------
143
144
 
145
+ 0.9.0 (23.05.2025)
146
+ ~~~~~~~~~~~~~~~~~~~
147
+
148
+ - Replaces `JSON` database type with `JSONB`, this means
149
+ Postgres as a backend is not required. You will also need
150
+ to write a migration for existing JSON columns. You may use
151
+ the following recipe using an alembic `Operations` object::
152
+
153
+ operations.alter_column(
154
+ 'table_name',
155
+ 'column_name',
156
+ type_=JSON,
157
+ postgresql_using='"column_name"::jsonb'
158
+ )
159
+
144
160
  0.8.0 (15.01.2025)
145
161
  ~~~~~~~~~~~~~~~~~~~
146
162
 
@@ -2,8 +2,8 @@ Libres
2
2
  ======
3
3
 
4
4
  Libres is a reservations management library to reserve things like tables at
5
- a restaurant or tickets at an event. It works with Python 3.8+
6
- and requires Postgresql 9.1+.
5
+ a restaurant or tickets at an event. It works with Python 3.9+
6
+ and requires Postgresql 9.2+.
7
7
 
8
8
  `Documentation <http://libres.readthedocs.org/en/latest/>`_ | `Source <http://github.com/seantis/libres/>`_ | `Bugs <http://github.com/seantis/libres/issues>`_
9
9
 
@@ -11,7 +11,7 @@ branch = true
11
11
  source = ["src"]
12
12
 
13
13
  [tool.bumpversion]
14
- current_version = "0.8.0"
14
+ current_version = "0.9.0"
15
15
  commit = true
16
16
  message = "Release {new_version}"
17
17
  tag = true
@@ -36,14 +36,12 @@ replace = """
36
36
 
37
37
  [tool.mypy]
38
38
  python_version = "3.9"
39
- follow_imports = "silent"
40
39
  namespace_packages = true
41
40
  explicit_package_bases = true
42
- warn_unused_ignores = true
43
- warn_redundant_casts = true
41
+ strict = true
44
42
  warn_unreachable = true
45
- disallow_any_generics = true
46
- disallow_untyped_defs = true
43
+ # FIXME: remove this exclusion when upgrading to SQlAlchemy 2.0
44
+ untyped_calls_exclude = "sqlalchemy"
47
45
  plugins = "sqlmypy"
48
46
  mypy_path = "$MYPY_CONFIG_FILE_DIR/src"
49
47
 
@@ -5,7 +5,7 @@ from libres.db import new_scheduler
5
5
 
6
6
  registry = create_default_registry()
7
7
 
8
- __version__ = '0.8.0'
8
+ __version__ = '0.9.0'
9
9
  __all__ = (
10
10
  'new_scheduler',
11
11
  'registry'
@@ -61,15 +61,15 @@ class ContextServicesMixin:
61
61
 
62
62
  @cached_property
63
63
  def is_allocation_exposed(self) -> Callable[[Allocation], bool]:
64
- return self.context.get_service('exposure').is_allocation_exposed
64
+ return self.context.get_service('exposure').is_allocation_exposed # type: ignore[no-any-return]
65
65
 
66
66
  @cached_property
67
67
  def generate_uuid(self) -> Callable[[str], UUID]:
68
- return self.context.get_service('uuid_generator')
68
+ return self.context.get_service('uuid_generator') # type: ignore[no-any-return]
69
69
 
70
70
  @cached_property
71
71
  def validate_email(self) -> Callable[[str], bool]:
72
- return self.context.get_service('email_validator')
72
+ return self.context.get_service('email_validator') # type: ignore[no-any-return]
73
73
 
74
74
  def clear_cache(self) -> None:
75
75
  """ Clears the cache of the mixin. """
@@ -91,12 +91,12 @@ class ContextServicesMixin:
91
91
 
92
92
  @property
93
93
  def session_provider(self) -> SessionProvider:
94
- return self.context.get_service('session_provider')
94
+ return self.context.get_service('session_provider') # type: ignore[no-any-return]
95
95
 
96
96
  @property
97
97
  def session(self) -> Session:
98
98
  """ Returns the current session. """
99
- return self.session_provider.session()
99
+ return self.session_provider.session() # type: ignore[no-any-return]
100
100
 
101
101
  def close(self) -> None:
102
102
  """ Closes the current session. """
@@ -98,7 +98,7 @@ class Registry:
98
98
  if not hasattr(self.local, 'current_context'):
99
99
  self.local.current_context = self.master_context
100
100
 
101
- return self.local.current_context
101
+ return self.local.current_context # type: ignore[no-any-return]
102
102
 
103
103
  def is_existing_context(self, name: str) -> bool:
104
104
  return name in self.contexts
@@ -34,14 +34,18 @@ if TYPE_CHECKING:
34
34
  from collections.abc import Iterator
35
35
  from sedate.types import TzInfoOrName
36
36
  from sqlalchemy.orm import Query
37
+ from typing import NamedTuple
37
38
  from typing_extensions import Self
38
39
 
39
- from libres.db.models import Reservation, ReservedSlot
40
+ from libres.db.models import ReservedSlot
40
41
  from libres.modules.rasterizer import Raster
41
42
 
42
43
  _OptDT1 = TypeVar('_OptDT1', 'datetime | None', datetime, None)
43
44
  _OptDT2 = TypeVar('_OptDT2', 'datetime | None', datetime, None)
44
45
 
46
+ class _ReservationIdRow(NamedTuple):
47
+ id: int
48
+
45
49
 
46
50
  class Allocation(TimestampMixin, ORMBase, OtherModels):
47
51
  """Describes a timespan within which one or many timeslots can be
@@ -114,7 +118,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
114
118
  timezone: Column[str | None] = Column(types.String())
115
119
 
116
120
  #: Custom data reserved for the user
117
- data: Column[Any | None] = Column(
121
+ data: Column[dict[str, Any] | None] = Column(
118
122
  JSON(),
119
123
  nullable=True
120
124
  )
@@ -470,7 +474,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
470
474
  return self.display_start(timezone), self.display_end(timezone)
471
475
 
472
476
  @property
473
- def pending_reservations(self) -> Query[Reservation]:
477
+ def pending_reservations(self) -> Query[_ReservationIdRow]:
474
478
  """ Returns the pending reservations query for this allocation.
475
479
  As the pending reservations target the group and not a specific
476
480
  allocation this function returns the same value for masters and
@@ -482,6 +486,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
482
486
  )
483
487
 
484
488
  Reservation = self.models.Reservation # noqa: N806
489
+ query: Query[_ReservationIdRow]
485
490
  query = object_session(self).query(Reservation.id)
486
491
  query = query.filter(Reservation.target == self.group)
487
492
  query = query.filter(Reservation.status == 'pending')
@@ -794,7 +799,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
794
799
  if self.is_master:
795
800
  return self
796
801
  else:
797
- query = object_session(self).query(Allocation)
802
+ # FIXME: This should either query `self.__class__` or
803
+ # we need to return `Allocation` rather than `Self`
804
+ query: Query[Self] = object_session(self).query(Allocation)
798
805
  query = query.filter(Allocation._start == self._start)
799
806
  query = query.filter(Allocation.resource == self.mirror_of)
800
807
 
@@ -818,7 +825,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
818
825
  assert self.is_master
819
826
  return [self]
820
827
 
821
- query = object_session(self).query(Allocation)
828
+ # FIXME: This should either query `self.__class__` or
829
+ # we need to return `Allocation` rather than `Self`
830
+ query: Query[Self] = object_session(self).query(Allocation)
822
831
  query = query.filter(Allocation.mirror_of == self.mirror_of)
823
832
  query = query.filter(Allocation._start == self._start)
824
833
 
@@ -96,7 +96,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
96
96
  nullable=False
97
97
  )
98
98
 
99
- data: Column[Any | None] = deferred(
99
+ data: Column[dict[str, Any] | None] = deferred(
100
100
  Column(
101
101
  JSON(),
102
102
  nullable=True
@@ -148,7 +148,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
148
148
  # order by date
149
149
  query = query.order_by(Allocation._start)
150
150
 
151
- return query
151
+ return query # type: ignore[no-any-return]
152
152
 
153
153
  def display_start(
154
154
  self,
@@ -212,4 +212,4 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
212
212
  # A reservation is deemed autoapprovable if no allocation
213
213
  # requires explicit approval
214
214
 
215
- return object_session(self).query(~query.exists()).scalar()
215
+ return object_session(self).query(~query.exists()).scalar() # type: ignore[no-any-return]
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.ext.mutable import MutableDict
4
+ from sqlalchemy.types import TypeDecorator
5
+ from sqlalchemy.dialects.postgresql import JSONB
6
+
7
+
8
+ from typing import Any
9
+ from typing import TYPE_CHECKING
10
+ if TYPE_CHECKING:
11
+ from sqlalchemy.engine import Dialect
12
+
13
+ _Base = TypeDecorator[dict[str, Any]]
14
+ else:
15
+ _Base = TypeDecorator
16
+
17
+
18
+ class JSON(_Base):
19
+ """ A JSONB based type that coerces None's to empty dictionaries.
20
+
21
+ That is, this JSONB column cannot be `'null'::jsonb`. It could
22
+ still be `NULL` though, if it's nullable and never explicitly
23
+ set. But on the Python end you should always see a dictionary.
24
+
25
+ """
26
+
27
+ impl = JSONB
28
+
29
+ def process_bind_param( # type:ignore[override]
30
+ self,
31
+ value: dict[str, Any] | None,
32
+ dialect: Dialect
33
+ ) -> dict[str, Any]:
34
+
35
+ return {} if value is None else value
36
+
37
+ def process_result_value(
38
+ self,
39
+ value: dict[str, Any] | None,
40
+ dialect: Dialect
41
+ ) -> dict[str, Any]:
42
+
43
+ return {} if value is None else value
44
+
45
+
46
+ MutableDict.associate_with(JSON) # type:ignore[no-untyped-call]
@@ -306,7 +306,10 @@ class Scheduler(ContextServicesMixin):
306
306
  ) -> list[tuple[datetime, datetime]]:
307
307
 
308
308
  query = self.allocations_by_group(group)
309
- dates_query = query.with_entities(Allocation._start, Allocation._end)
309
+ dates_query: Query[tuple[datetime, datetime]] = query.with_entities(
310
+ Allocation._start,
311
+ Allocation._end
312
+ )
310
313
  return dates_query.all()
311
314
 
312
315
  def allocation_mirrors_by_master(
@@ -336,7 +339,7 @@ class Scheduler(ContextServicesMixin):
336
339
  query = self.allocations_by_ids(ids)
337
340
  query = query.filter(Allocation.approve_manually == True)
338
341
 
339
- return self.session.query(query.exists()).scalar()
342
+ return self.session.query(query.exists()).scalar() # type: ignore[no-any-return]
340
343
 
341
344
  def allocate(
342
345
  self,
@@ -1273,8 +1276,9 @@ class Scheduler(ContextServicesMixin):
1273
1276
  # reservation twice on a single session
1274
1277
  if session_id:
1275
1278
  found = self.queries.reservations_by_session(session_id)
1276
- found = found.with_entities(Reservation.target, Reservation.start)
1277
- found_set = set(found)
1279
+ found_set: set[tuple[UUID, datetime | None]] = set(
1280
+ found.with_entities(Reservation.target, Reservation.start)
1281
+ )
1278
1282
 
1279
1283
  for reservation in reservations:
1280
1284
  if (reservation.target, reservation.start) in found_set:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: libres
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A library to reserve things
5
5
  Home-page: http://github.com/seantis/libres/
6
6
  Author: Denis Krienbühl
@@ -51,13 +51,14 @@ Requires-Dist: types-psycopg2; extra == "mypy"
51
51
  Requires-Dist: types-python-dateutil; extra == "mypy"
52
52
  Requires-Dist: types-pytz; extra == "mypy"
53
53
  Requires-Dist: typing-extensions; extra == "mypy"
54
+ Dynamic: license-file
54
55
 
55
56
  Libres
56
57
  ======
57
58
 
58
59
  Libres is a reservations management library to reserve things like tables at
59
- a restaurant or tickets at an event. It works with Python 3.8+
60
- and requires Postgresql 9.1+.
60
+ a restaurant or tickets at an event. It works with Python 3.9+
61
+ and requires Postgresql 9.2+.
61
62
 
62
63
  `Documentation <http://libres.readthedocs.org/en/latest/>`_ | `Source <http://github.com/seantis/libres/>`_ | `Bugs <http://github.com/seantis/libres/issues>`_
63
64
 
@@ -141,6 +142,21 @@ After this, create a new release on Github.
141
142
  Changelog
142
143
  ---------
143
144
 
145
+ 0.9.0 (23.05.2025)
146
+ ~~~~~~~~~~~~~~~~~~~
147
+
148
+ - Replaces `JSON` database type with `JSONB`, this means
149
+ Postgres as a backend is not required. You will also need
150
+ to write a migration for existing JSON columns. You may use
151
+ the following recipe using an alembic `Operations` object::
152
+
153
+ operations.alter_column(
154
+ 'table_name',
155
+ 'column_name',
156
+ type_=JSON,
157
+ postgresql_using='"column_name"::jsonb'
158
+ )
159
+
144
160
  0.8.0 (15.01.2025)
145
161
  ~~~~~~~~~~~~~~~~~~~
146
162
 
@@ -1821,7 +1821,7 @@ def test_data_coding(scheduler):
1821
1821
  scheduler.allocate((start, end), data=None)
1822
1822
  scheduler.commit()
1823
1823
 
1824
- assert scheduler.managed_allocations().first().data is None
1824
+ assert scheduler.managed_allocations().first().data == {}
1825
1825
 
1826
1826
 
1827
1827
  def test_no_reservations_to_confirm(scheduler):
@@ -1,51 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from json import loads, dumps
4
- from sqlalchemy.types import TypeDecorator, TEXT
5
-
6
-
7
- from typing import Any
8
- from typing import TYPE_CHECKING
9
- if TYPE_CHECKING:
10
- from sqlalchemy.engine import Dialect
11
-
12
- _Base = TypeDecorator[Any]
13
- else:
14
- _Base = TypeDecorator
15
-
16
-
17
- class JSON(_Base):
18
- """Like the default JSON, but using the json serializer from the dialect
19
- (postgres) each time the value is read, even if it never left the ORM. The
20
- default json type will only do it when the record is read from the
21
- database.
22
-
23
- """
24
-
25
- # Use TEXT for now to stay compatible with Postgres 9.1. In the future
26
- # this will be replaced by JSON (or JSONB) though that requires that we
27
- # require a later Postgres release. For now we stay backwards compatible
28
- # with a version that's still widely used (9.1).
29
- impl = TEXT
30
-
31
- def process_bind_param(
32
- self,
33
- value: Any,
34
- dialect: Dialect
35
- ) -> str | None:
36
-
37
- if value is not None:
38
- value = (dialect._json_serializer or dumps)(value) # type:ignore
39
-
40
- return value
41
-
42
- def process_result_value(
43
- self,
44
- value: str | None,
45
- dialect: Dialect
46
- ) -> Any | None:
47
-
48
- if value is not None:
49
- value = (dialect._json_deserializer or loads)(value) # type:ignore
50
-
51
- return value
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