Metadata-Version: 2.4
Name: libres
Version: 1.1.1
Summary: A library to reserve things
Author-email: Denis Krienbühl <denis@href.ch>
Maintainer-email: Seantis GmbH <info@seantis.ch>
License-Expression: MIT
Project-URL: Repository, http://github.com/seantis/libres
Classifier: Framework :: Pytest
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: python-dateutil
Requires-Dist: pytz
Requires-Dist: sedate>=1.0.0
Requires-Dist: SQLAlchemy<2.1.0,>=2.0.0
Dynamic: license-file

Libres
======

Libres is a reservations management library to reserve things like tables at
a restaurant or tickets at an event. It works with Python 3.10+
and requires Postgresql 9.2+.

`Documentation <http://libres.readthedocs.org/en/latest/>`_ | `Source <http://github.com/seantis/libres/>`_ | `Bugs <http://github.com/seantis/libres/issues>`_


.. image:: https://github.com/seantis/libres/actions/workflows/python-tox.yaml/badge.svg
  :target: https://github.com/seantis/libres/actions
  :alt:    Tests

.. image:: https://codecov.io/gh/seantis/libres/branch/master/graph/badge.svg?token=2WZfY5HwdE
  :target: https://codecov.io/gh/seantis/libres
  :alt:    Coverage

.. image:: https://img.shields.io/pypi/v/libres.svg
  :target: https://pypi.python.org/pypi/libres
  :alt:    Release

.. < package description

Run the Example
---------------

Go to examples/flask and install the requirements::

    cd examples/flask
    pip install -r requirements.txt

Run the example::

    python run.py

Open http://localhost:5000 and click around.

Run the Tests
-------------

Install tox and run it::

    pip install tox tox-uv
    tox

Limit the tests to a specific python version::

    tox -e py311

Conventions
-----------

Libres follows PEP8 as close as possible. To test for it run::

    tox -e ruff,flake8

Libres uses `Semantic Versioning <http://semver.org/>`_

Build the Docs
--------------

Go to docs and install the requirements::

    cd docs
    pip install -r requirements.txt

Build the docs::

    make html

Open the docs::

    open build/html/index.html

Making a new Release
--------------------

Make sure all changes are in the HISTORY.rst, then bump the version::

    bump-my-version bump major|minor|patch
    git push && git push --tags

After this, create a new release on Github.

Changelog
---------

1.1.1 (27.05.2026)
~~~~~~~~~~~~~~~~~~~

- Fixes bug in `Registry.context` context manager, that could result
  in the outer context not getting restored, if an exception
  occurred in the `with` block.
  [Daverball]

- Adds additional indexes to speed up DB operations, you can use
  the following recipe using an alembic ``Operations`` object to
  migrate existing databases::

    context.operations.create_index(
      'ix_reserved_slots_end',
      'reserved_slots',
      columns=['end'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reserved_slots_allocation_id',
      'reserved_slots',
      columns=['allocation_id'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_token',
      'reservations',
      columns=['token'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_target',
      'reservations',
      columns=['target'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_target_type',
      'reservations',
      columns=['target_type'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_type',
      'reservations',
      columns=['type'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_resource',
      'reservations',
      columns=['resource'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_start',
      'reservations',
      columns=['start'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_end',
      'reservations',
      columns=['end'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_status',
      'reservations',
      columns=['status'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_email',
      'reservations',
      columns=['email'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservations_session_id',
      'reservations',
      columns=['session_id'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_token',
      'reservation_blockers',
      columns=['token'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_target',
      'reservation_blockers',
      columns=['target'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_target_type',
      'reservation_blockers',
      columns=['target_type'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_resource',
      'reservation_blockers',
      columns=['resource'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_start',
      'reservation_blockers',
      columns=['start'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_reservation_blockers_end',
      'reservation_blockers',
      columns=['end'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_resource',
      'allocations',
      columns=['resource'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_type',
      'allocations',
      columns=['type'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_mirror_of',
      'allocations',
      columns=['mirror_of'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_group',
      'allocations',
      columns=['group'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_start',
      'allocations',
      columns=['_start'],
      if_not_exists=True
    )
    context.operations.create_index(
      'ix_allocations_end',
      'allocations',
      columns=['_end'],
      if_not_exists=True
    )

1.1.0 (08.04.2026)
~~~~~~~~~~~~~~~~~~~

- Adds support for blocking resources. ``Scheduler`` now accepts a collection
  of ``blocking_names`` to indicate related resources that will block reservations
  on the managed resource (e.g. for a soccer field where you can either reserve
  the entire field or one of the two halves, the two halves should block the
  entire field and vice versa, but the halves shouldn't block each other).

  For schedulers with blocking resources you should use the new method
  ``allocations_with_availability_by_range`` to retrieve the availability and
  availability partitions for a set of allocations, since the corresponding
  attributes/method on the ``Allocation`` will be inaccurate when blocking
  resources are involved.

  For decent performance it's absolutely vital that the new GiST index on
  ``tsrange(reserved_slots.start, reserved_slots."end")`` exists, it's also worth
  adding a potentially missing index on the ``source_type`` column, you can
  use the following recipe using an alembic ``Operations`` object to migrate
  existing databases::

    context.operations.create_index(
      'ix_reserved_slots_source_type',
      'reserved_slots',
      columns=['source_type'],
      if_not_exists=True
    )
    context.operations.create_index(
      'start_end_tsrange_ix',
      'reserved_slots',
      columns=[text('tsrange(start, "end")')],
      postgresql_using='gist',
      if_not_exists=True
    )

1.0.0 (10.02.2026)
~~~~~~~~~~~~~~~~~~~

- Adds support for Python 3.14
  [Daverball]

- Adds support for SQLAlchemy 2.0
  [Daverball]

- Drops support for Python 3.9
  [Daverball]

- Drops support for SQLAlchemy 1.4
  [Daverball]

0.10.2 (05.02.2026)
~~~~~~~~~~~~~~~~~~~

- Prepares for SQLAlchemy 2.0 support
  [Daverball]

0.10.1 (21.01.2026)
~~~~~~~~~~~~~~~~~~~

- Adds proper support for SQLAlchemy 1.4. As a result of this
  ``Allocation.type`` and ``Reservation.type`` are no longer nullable
  and have a default value of ``'generic'``, you may use the following
  recipe using an alembic ``Operations`` object to migrate existing
  databases::

    context.operations.execute("""
      UPDATE allocations
         SET type = 'generic'
       WHERE type IS NULL;
    """)
    context.operations.alter_column('allocations', 'type', nullable=False)
    context.operations.execute("""
      UPDATE reservations
         SET type = 'generic'
       WHERE type IS NULL;
    """)
    context.operations.alter_column('reservations', 'type', nullable=False)

0.10.0 (15.01.2026)
~~~~~~~~~~~~~~~~~~~

- Adds new entity ``ReservationBlocker`` for administrative blockers
  of targeted allocations for the targeted timespans, this also ends
  up adding a new column ``source_type`` to ``ReservedSlot`` which can be
  added using the following recipe using an alembic ``Operations`` object::

    operations.add_column(
      'reserved_slots',
      Column(
        'source_type',
        Enum(
            'reservation', 'blocker',
            name='reserved_slot_source_type'
        ),
        nullable=False,
        server_default='reservation'
      )
    )
    operations.alter_column(
      'reserved_slots',
      'source_type',
      server_default=None
    )


0.9.1 (05.08.2025)
~~~~~~~~~~~~~~~~~~~

- Fixes bug in ``Scheduler.search_allocations`` when the searched
  time range contains multiple DST <-> ST transitions.
  [Daverball]

0.9.0 (23.05.2025)
~~~~~~~~~~~~~~~~~~~

- Replaces ``JSON`` database type with ``JSONB``, this means
  Postgres as a backend is not required. You will also need
  to write a migration for existing JSON columns. You may use
  the following recipe using an alembic ``Operations`` object::

    operations.alter_column(
      'table_name',
      'column_name',
      type_=JSON,
      postgresql_using='"column_name"::jsonb'
    )

0.8.0 (15.01.2025)
~~~~~~~~~~~~~~~~~~~

- Adds support for Python 3.13
  [Daverball]

- Drops support for Python 3.8
  [Daverball]

- Modernizes type hints
  [Daverball]

0.7.3 (2024-08-21)
~~~~~~~~~~~~~~~~~~~

- Adds support for Python 3.12.
  [Daverball]

0.7.2 (2024-02-07)
~~~~~~~~~~~~~~~~~~~

- Fixes another incorrect type annotation in ``Scheduler``.
  [Daverball]

0.7.1 (2024-01-18)
~~~~~~~~~~~~~~~~~~~

- Fixes some incorrect type annotations in ``Scheduler``.
  [Daverball]

0.7.0 (2023-07-11)
~~~~~~~~~~~~~~~~~~~

- Drops support for Python 3.7 and adds support for 3.11
  [Daverball]

- Switches to ``pyproject.toml``
  [Daverball]

- Adds type annotations
  [Daverball]

- Changes ``Scheduler.allocate`` to avoid hundreds of separate
  SQL queries when passing in hundreds of datetime ranges in
  order to identify existing overlapping allocations.

  Performance could still be a concern, since the query contains
  a lot of datetime comparisons. It might be quicker in the common case to filter to the minimum and maximum dates that
  have been passed in and doing the overlap checks entirely in
  Python. We will need to keep an eye on this.
  [Daverball]

0.6.1 (2023-03-29)
~~~~~~~~~~~~~~~~~~~

- Adds additional parameters to ``Scheduler.remove_unused allocations``
  to filter the to be removed Allocations by weekday or
  whether or not they belong to a group.
  [Daverball]

- Fixes bug in ``Scheduler.search_allocations``. It did not
  align the days parameter properly to the timezone of the
  Allocation/Scheduler.
  [Daverball]

- Pins SQLAlchemy to versions before 2.0
  [Daverball]

0.6.0 (2022-08-10)
~~~~~~~~~~~~~~~~~~~

- Drops Python 3.6 support.
  [Daverball]

- Normalizes availability partitions on 23/25 hours to a 24 hour day
  so that DST transition days can be rendered the same as regular days.

  This can optionally be avoided by passing ``normalize_dst=False`` to
  the function.
  [Daverball]

- Adds ``Allocation.normalized_availability`` that reports the
  availability in the same normalized way.
  [Daverball]

- Adds extra parameters to ``Allocation.limit_timespan`` that match
  the new parameters added to ``sedate.get_date_range``.
  [Daverball]

0.5.4 (2022-06-15)
~~~~~~~~~~~~~~~~~~~

- Switches from Travis to GitHub Workflows.
  [msom]

- Resolves SQLAlchemy 1.4 warnings.
  [msom]

0.5.3 (2021-01-18)
~~~~~~~~~~~~~~~~~~~

- Fix collections deprecation warnings and fix tests.
  [dadadamotha]

0.5.2 (2019-11-11)
~~~~~~~~~~~~~~~~~~~

- Adds Python 3.8 compatibility.
  [href]

- Changes PostgreSQL version check to be distribution-independent.
  [href]

0.5.1 (2019-01-23)
~~~~~~~~~~~~~~~~~~~

- Fixes overlapping allocations not being skipped in all cases.
  [href]

0.5.0 (2019-01-17)
~~~~~~~~~~~~~~~~~~~

- Adds the ability to skip overlapping allocations.
  [href]

0.4.0 (2017-06-16)
~~~~~~~~~~~~~~~~~~~

- Adds the ability to define the allocation/reservation class used by the
  scheduler.

0.3.1 (2017-06-07)
~~~~~~~~~~~~~~~~~~~

- Approved reservations may have session ids again, this restores backwards
  compatibiility with seantis.reservation.
  [href]

0.3.0 (2017-06-07)
~~~~~~~~~~~~~~~~~~~

- Enables polymorphy on reservations and allocations.
  [href]

0.2.4 (2017-01-10)
~~~~~~~~~~~~~~~~~~~

- Fixes reservation length check not working on DST days.
  [href]

0.2.3 (2016-10-25)
~~~~~~~~~~~~~~~~~~~

- Small performance improvements when dealing with many allocations.
  [href]

0.2.2 (2016-07-08)
~~~~~~~~~~~~~~~~~~~

- Ensures that all models are hashable to avoid problems with certain
  SQLAlchemy extensions/plugins.
  [href]

0.2.1 (2016-04-27)
~~~~~~~~~~~~~~~~~~~

- Reservations added to the same session may not be duplicated anymore.
  [href]

- Errors raised during reservation now have a 'reservation' attribute.
  [href]

0.2.0 (2016-04-26)
~~~~~~~~~~~~~~~~~~~

- Adds the ability to have a single token shared across multiple reservations
  in a single session.
  [href]

0.1.4 (2015-11-25)
~~~~~~~~~~~~~~~~~~~

- Adds the ability to change unapproved reservations.
  [href]

- Adds an extra check for start/end time. If the requested start/end time lies
  outside any possible allocation, an error is raised.
  [href]

- Ensures that approved reservations cannot be somehow removed during cleanup.
  [href]

0.1.3 (2015-09-03)
~~~~~~~~~~~~~~~~~~

- Adds a method to remove unused allocations.
  [href]

0.1.2 (2015-08-25)
~~~~~~~~~~~~~~~~~~

- Replaces libres.modules.calendar with sedate.
  [href]

- Naive start/end dates on the allocation are now automatically converted into
  the correct timezone when they are set.
  [href]

0.1.1 (2015-08-19)
~~~~~~~~~~~~~~~~~~

- It was possible to add or move an allocation to an invalid state (end before
  start date). This is now caught correctly.
  [href]

0.1.0 (2015-07-30)
~~~~~~~~~~~~~~~~~~

- BREAKING CHANGE: This release switches to a single SERIALIZED connections.

  Previously it used a READ COMMITED and a SERIALIZED connection in parallel,
  switching to the READ COMMITED connection for readonly queries and using
  the SERIALIZED connection for write queries.

  Using a serialized connection for everything reduces speed slightly (though
  we haven't been able to measure the effect on our lowish traffic sites). But
  it makes it easier to use libres with an existing connection when integrating
  it.

  It also simplifies the code by quite a bit.

0.0.2 (2015-03-16)
~~~~~~~~~~~~~~~~~~

- Fix being unable to delete an allocation with a quota > 1.
  See issue #8.
  [href]

- Replace read session write guard with a simpler version.
  [href]

0.0.1 (2015-02-09)
~~~~~~~~~~~~~~~~~~

- Initial release.
  [href]
