diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edd7300c..f2e4333b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,8 +17,51 @@ on: - ".github/ISSUE_TEMPLATE/**" jobs: + # Derive the MEOS catalog once from the MobilityDB branch the package version + # maps to, through the shared provision-meos action. PyMEOS-CFFI projects its + # cdef header from this catalog, so every test job builds the same surface. + # The catalog parses the headers a libmeos build installs, as PyMEOS-CFFI's + # pr_build does, so it declares no internal header the extension cannot include. + catalog: + name: Derive the MEOS catalog + runs-on: ubuntu-latest + outputs: + meos_branch: ${{ steps.meos_branch.outputs.meos_branch }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Derive MEOS branch from package version + id: meos_branch + shell: bash + run: | + pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) + major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') + candidate="stable-${major_minor}" + if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then + meos_branch=$candidate + else + meos_branch="master" + fi + echo "Package version $pymeos_version => MEOS branch $meos_branch" + echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" + + - name: Provision the MEOS catalog + id: provision + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master + with: + mobilitydb-ref: ${{ steps.meos_branch.outputs.meos_branch }} + build-libmeos: "true" + + - name: Upload the catalog + uses: actions/upload-artifact@v4 + with: + name: meos-idl + path: ${{ steps.provision.outputs.catalog-path }} + test: name: Test PyMEOS - Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: catalog runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -34,18 +77,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Get dependencies from apt (cache) - uses: awalsh128/cache-apt-pkgs-action@v1.4.3 + # The CFFI cdef covers every optional MEOS family, so libmeos is built + # with all of them: on Linux by the provision-meos action, from the same + # MobilityDB branch as the catalog. + - name: Build and install the all-families MEOS (Linux) if: runner.os == 'Linux' + uses: MobilityDB/MEOS-API/.github/actions/provision-meos@master with: - packages: build-essential cmake postgresql-server-dev-16 libproj-dev libjson-c-dev libgsl-dev libgeos-dev - version: 1.1 + mobilitydb-ref: ${{ needs.catalog.outputs.meos_branch }} + build-libmeos: "true" - name: Get dependencies from homebrew (cache) uses: tecolicom/actions-use-homebrew-tools@v1 if: runner.os == 'macOS' with: - tools: cmake libpq proj json-c gsl geos + tools: cmake libpq proj json-c gsl geos h3 gdal - name: Update brew if: matrix.os == 'macos-15-intel' @@ -56,31 +102,14 @@ jobs: brew reinstall python@3.11 || brew link --overwrite python@3.11 brew update - - name: Derive MEOS branch from package version - id: meos_branch - shell: bash - run: | - pymeos_version=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' pymeos/__init__.py) - major_minor=$(echo "$pymeos_version" | sed -nE 's/^([0-9]+\.[0-9]+).*/\1/p') - candidate="stable-${major_minor}" - if git ls-remote --exit-code https://github.com/MobilityDB/MobilityDB "$candidate" >/dev/null 2>&1; then - meos_branch=$candidate - else - meos_branch="master" - fi - echo "Package version $pymeos_version => MEOS branch $meos_branch" - echo "meos_branch=$meos_branch" >> "$GITHUB_OUTPUT" - - - name: Fetch MEOS sources - run: | - git clone --branch ${{ steps.meos_branch.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB - - - name: Install MEOS + - name: Build and install the all-families MEOS (macOS) + if: runner.os == 'macOS' run: | + git clone --branch ${{ needs.catalog.outputs.meos_branch }} --depth 1 https://github.com/MobilityDB/MobilityDB mkdir MobilityDB/build cd MobilityDB/build - cmake .. -DMEOS=on -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} - make -j + cmake .. -DMEOS=ON -DALL=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${{ matrix.ld_prefix }} + make -j"$(sysctl -n hw.ncpu)" sudo make install - name: Set up Python ${{ matrix.python-version }} @@ -96,6 +125,12 @@ jobs: git clone --branch ${{ env.BRANCH_NAME }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI \ || git clone --branch ${{ github.base_ref || 'master' }} --depth 1 https://github.com/MobilityDB/PyMEOS-CFFI + - name: Download the catalog + uses: actions/download-artifact@v4 + with: + name: meos-idl + path: PyMEOS-CFFI/builder + - name: Install python dependencies run: | python -m pip install --upgrade pip @@ -103,12 +138,25 @@ jobs: pip install -r dev-requirements.txt - name: Install pymeos_cffi + shell: bash run: | cd PyMEOS-CFFI - python ./builder/build_header.py + python ./builder/build_header.py builder/meos-idl.json \ + ${{ matrix.ld_prefix }}/lib/${{ runner.os == 'macOS' && 'libmeos.dylib' || 'libmeos.so' }} builder/meos.h python ./builder/build_pymeos_functions.py pip install . + - name: Portable bare-name parity gate + # Regenerate pymeos/portable.py from the MEOS just built+installed so + # the shipped module matches the binding under test, and hard-gate + # the canonical contract: 29/29 bare names backed, 0 unbacked, all + # six families, and the live pymeos_cffi backs every one (--cffi). + run: | + export MEOS_INCLUDE=${{ matrix.ld_prefix }}/include + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib + python tools/portable_aliases/generate.py --cffi --check + - name: Test PyMEOS with pytest run: | export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{ matrix.ld_prefix }}/lib diff --git a/pymeos/__init__.py b/pymeos/__init__.py index 1924c21b..004ad8ab 100644 --- a/pymeos/__init__.py +++ b/pymeos/__init__.py @@ -30,7 +30,7 @@ from .meos_init import * from .temporal import * -__version__ = "1.3.0-alpha-1" +__version__ = "1.4.0-alpha-1" __all__ = [ # initialization "pymeos_initialize", diff --git a/pymeos/aggregators/aggregator.py b/pymeos/aggregators/aggregator.py index be86e04d..96d0dc63 100644 --- a/pymeos/aggregators/aggregator.py +++ b/pymeos/aggregators/aggregator.py @@ -243,12 +243,12 @@ def _add( interval_converted = ( timedelta_to_interval(interval) if isinstance(interval, timedelta) - else pg_interval_in(interval, -1) + else interval_in(interval, -1) if isinstance(interval, str) else None ) origin_converted = ( - datetime_to_timestamptz(origin) if isinstance(origin, datetime) else pg_timestamptz_in(origin, -1) + datetime_to_timestamptz(origin) if isinstance(origin, datetime) else timestamptz_in(origin, -1) ) return cls._add_function(state, temporal._inner, interval_converted, origin_converted) diff --git a/pymeos/aggregators/time_aggregators.py b/pymeos/aggregators/time_aggregators.py index 159503d4..a730ae8e 100644 --- a/pymeos/aggregators/time_aggregators.py +++ b/pymeos/aggregators/time_aggregators.py @@ -4,9 +4,10 @@ datetime_to_timestamptz, set_union_finalfn, set_union_transfn, + span_union_transfn, + spanset_union_finalfn, + spanset_union_transfn, timestamptz_union_transfn, - union_spanset_span, - union_spanset_spanset, ) from ..collections import TsTzSet, TsTzSpan, TsTzSpanSet @@ -43,20 +44,24 @@ class TimeContinuousUnionAggregator(BaseAggregator[TsTzSpan | TsTzSpanSet, TsTzS Temporal union of continuous time objects (:class:`~pymeos.time.tstzspan.TsTzSpan` and :class:`~pymeos.time.tstzspanset.TsTzSpanSet`). + Aggregates as #TimeInstantaneousUnionAggregator does, through the transition and final + functions MEOS provides for the union of spans. + MEOS Functions: - union_spanset_span, union_spanset_spanset + span_union_transfn, spanset_union_transfn, spanset_union_finalfn """ @classmethod def _add(cls, state, temporal): if isinstance(temporal, TsTzSpan): - state = union_spanset_span(state, temporal._inner) + state = span_union_transfn(state, temporal._inner) elif isinstance(temporal, TsTzSpanSet): - state = union_spanset_spanset(state, temporal._inner) + state = spanset_union_transfn(state, temporal._inner) else: cls._error(temporal) return state @classmethod def _finish(cls, state) -> TsTzSpanSet: - return TsTzSpanSet(_inner=state) + result = spanset_union_finalfn(state) + return TsTzSpanSet(_inner=result) diff --git a/pymeos/boxes/stbox.py b/pymeos/boxes/stbox.py index 9d24f7bc..e53b1b4b 100644 --- a/pymeos/boxes/stbox.py +++ b/pymeos/boxes/stbox.py @@ -178,7 +178,7 @@ def from_geometry(geom: shp.BaseGeometry, geodetic: bool = False) -> STBox: A new :class:`STBox` instance. MEOS Functions: - pgis_geometry_in, geo_to_stbox + geom_in, geog_in, geo_to_stbox """ gs = geo_to_gserialized(geom, geodetic) return STBox(_inner=geo_to_stbox(gs)) @@ -1157,15 +1157,15 @@ def nearest_approach_distance(self, other: shp.BaseGeometry | STBox | TPoint) -> ``self`` and ``other``. MEOS Functions: - nad_stbox_geo, nad_stbox_stbox + nad_stbox_geo, nad_stbox_stbox, nad_tgeo_stbox """ if isinstance(other, shp.BaseGeometry): gs = geo_to_gserialized(other, self.geodetic()) - return nad_stbox_geo(self._inner, gs) + return nad_stbox_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_stbox_stbox(self._inner, other._inner) + return nad_stbox_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tpoint_stbox(other._inner, self._inner) + return nad_tgeo_stbox(other._inner, self._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -1285,7 +1285,7 @@ def tile( dt = ( timedelta_to_interval(duration) if isinstance(duration, timedelta) - else pg_interval_in(duration, -1) + else interval_in(duration, -1) if isinstance(duration, str) else None ) @@ -1293,9 +1293,9 @@ def tile( datetime_to_timestamptz(start) if isinstance(start, datetime) else ( - pg_timestamptz_in(start, -1) + timestamptz_in(start, -1) if isinstance(start, str) - else pg_timestamptz_in("2000-01-03", -1) + else timestamptz_in("2000-01-03", -1) if self.has_t() else 0 ) @@ -1303,7 +1303,7 @@ def tile( gs = ( geo_to_gserialized(origin, self.geodetic()) if origin is not None - else (pgis_geography_in("Point(0 0 0)", -1) if self.geodetic() else pgis_geometry_in("Point(0 0 0)", -1)) + else (geog_in("Point(0 0 0)", -1) if self.geodetic() else geom_in("Point(0 0 0)", -1)) ) tiles, count = stbox_space_time_tiles(self._inner, sz, sz, sz, dt, gs, st, border_inc) return [STBox(_inner=tiles + i) for i in range(count)] diff --git a/pymeos/boxes/tbox.py b/pymeos/boxes/tbox.py index 336ecca1..bc4b2967 100644 --- a/pymeos/boxes/tbox.py +++ b/pymeos/boxes/tbox.py @@ -1086,16 +1086,16 @@ def tile( MEOS Functions: tintbox_tile_list, tfloabox_tile_list """ - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) st = ( datetime_to_timestamptz(start) if isinstance(start, datetime) - else (pg_timestamptz_in(start, -1) if isinstance(start, str) else pg_timestamptz_in("2000-01-03", -1)) + else (timestamptz_in(start, -1) if isinstance(start, str) else timestamptz_in("2000-01-03", -1)) ) if self._is_float(): - tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st) + tiles, count = tfloatbox_value_time_tiles(self._inner, size, dt, origin, st, border_inc=True) else: - tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st) + tiles, count = tintbox_value_time_tiles(self._inner, int(size), dt, int(origin), st, border_inc=True) return [TBox(_inner=tiles + c) for c in range(count)] # ------------------------- Comparisons ----------------------------------- diff --git a/pymeos/collections/base/spanset.py b/pymeos/collections/base/spanset.py index 232fb5f3..9b63c0d0 100644 --- a/pymeos/collections/base/spanset.py +++ b/pymeos/collections/base/spanset.py @@ -211,7 +211,7 @@ def spans(self) -> list[Span]: MEOS Functions: spanset_spans """ - return spanset_spanarr(self._inner) + return spanset_spanarr(self._inner)[0] def __hash__(self) -> int: """ diff --git a/pymeos/collections/geo/geoset.py b/pymeos/collections/geo/geoset.py index 5b794d0a..fd3efde4 100644 --- a/pymeos/collections/geo/geoset.py +++ b/pymeos/collections/geo/geoset.py @@ -161,7 +161,7 @@ def elements(self) -> list[shp.Geometry]: MEOS Functions: geoset_values """ - elems = geoset_values(self._inner) + elems = geoset_values(self._inner)[0] return [gserialized_to_shapely_geometry(elems[i]) for i in range(self.num_elements())] def srid(self) -> int: diff --git a/pymeos/collections/number/floatset.py b/pymeos/collections/number/floatset.py index f1332d30..23a77774 100644 --- a/pymeos/collections/number/floatset.py +++ b/pymeos/collections/number/floatset.py @@ -141,7 +141,7 @@ def elements(self) -> list[float]: MEOS Functions: floattset_values """ - elems = floatset_values(self._inner) + elems = floatset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/number/intset.py b/pymeos/collections/number/intset.py index a512c7d5..299ad444 100644 --- a/pymeos/collections/number/intset.py +++ b/pymeos/collections/number/intset.py @@ -140,7 +140,7 @@ def elements(self) -> list[int]: MEOS Functions: inttset_values """ - elems = intset_values(self._inner) + elems = intset_values(self._inner)[0] return [elems[i] for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------------ diff --git a/pymeos/collections/text/textset.py b/pymeos/collections/text/textset.py index 0a1caddf..28eb0e8b 100644 --- a/pymeos/collections/text/textset.py +++ b/pymeos/collections/text/textset.py @@ -95,7 +95,7 @@ def element_n(self, n: int): textset_value_n """ super().element_n(n) - return text2cstring(textset_value_n(self._inner, n + 1)[0]) + return text_to_cstring(textset_value_n(self._inner, n + 1)[0]) def elements(self): """ @@ -107,8 +107,8 @@ def elements(self): MEOS Functions: textset_values """ - elems = textset_values(self._inner) - return [text2cstring(elems[i]) for i in range(self.num_elements())] + elems = textset_values(self._inner)[0] + return [text_to_cstring(elems[i]) for i in range(self.num_elements())] # ------------------------- Topological Operations -------------------------------- @@ -224,7 +224,7 @@ def subtract_from(self, other: str) -> str | None: :meth:`minus` """ result = minus_text_set(other, self._inner) - return text2cstring(result[0]) if result is not None else None + return text_to_cstring(result[0]) if result is not None else None def union(self, other: TextSet | str) -> TextSet: """ diff --git a/pymeos/collections/time/dateset.py b/pymeos/collections/time/dateset.py index 0675eeb2..29e64672 100644 --- a/pymeos/collections/time/dateset.py +++ b/pymeos/collections/time/dateset.py @@ -8,6 +8,7 @@ before_set_date, contains_set_date, date_adt_to_date, + date_in, date_to_date_adt, dateset_end_value, dateset_in, @@ -26,7 +27,6 @@ minus_set_set, overafter_set_date, overbefore_set_date, - pg_date_in, union_set_date, union_set_set, ) @@ -63,7 +63,7 @@ class DateSet(Set[date], TimeCollection[date]): _mobilitydb_name = "dateset" _parse_function = dateset_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = dateset_make # ------------------------- Constructors ---------------------------------- @@ -162,7 +162,7 @@ def elements(self) -> list[date]: MEOS Functions: dateset_values """ - tss = dateset_values(self._inner) + tss = dateset_values(self._inner)[0] return [date_adt_to_date(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- diff --git a/pymeos/collections/time/datespan.py b/pymeos/collections/time/datespan.py index 934e2529..45f24c3b 100644 --- a/pymeos/collections/time/datespan.py +++ b/pymeos/collections/time/datespan.py @@ -9,6 +9,7 @@ before_span_date, contains_span_date, date_adt_to_date, + date_in, date_to_date_adt, datespan_duration, datespan_in, @@ -30,7 +31,6 @@ minus_span_spanset, overafter_span_date, overbefore_span_date, - pg_date_in, union_span_date, ) @@ -69,7 +69,7 @@ class DateSpan(Span[date], TimeCollection[date]): _mobilitydb_name = "datespan" _parse_function = datespan_in - _parse_value_function = lambda x: pg_date_in(x) if isinstance(x, str) else date_to_date_adt(x) + _parse_value_function = lambda x: date_in(x) if isinstance(x, str) else date_to_date_adt(x) _make_function = datespan_make # ------------------------- Constructors ---------------------------------- diff --git a/pymeos/collections/time/tstzset.py b/pymeos/collections/time/tstzset.py index 545b885e..08d0fa15 100644 --- a/pymeos/collections/time/tstzset.py +++ b/pymeos/collections/time/tstzset.py @@ -39,7 +39,7 @@ class TsTzSet(Set[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzset" _parse_function = tstzset_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzset_make # ------------------------- Constructors ---------------------------------- @@ -116,7 +116,7 @@ def elements(self) -> list[datetime]: MEOS Functions: set_timestamptzs """ - tss = tstzset_values(self._inner) + tss = tstzset_values(self._inner)[0] return [timestamptz_to_datetime(tss[i]) for i in range(self.num_elements())] # ------------------------- Transformations ------------------------------- @@ -512,9 +512,9 @@ def distance(self, other: Time | Temporal | Box) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_set_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): - return timedelta(seconds=distance_tstzset_tstzset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzset_tstzset(self._inner, other._inner)) elif isinstance(other, (TsTzSpan, TsTzSpanSet)): return self.to_spanset().distance(other) elif isinstance(other, (Temporal, get_args(Box))): diff --git a/pymeos/collections/time/tstzspan.py b/pymeos/collections/time/tstzspan.py index 966e5c98..62c8b5d9 100644 --- a/pymeos/collections/time/tstzspan.py +++ b/pymeos/collections/time/tstzspan.py @@ -41,7 +41,7 @@ class TsTzSpan(Span[datetime], TimeCollection[datetime]): _mobilitydb_name = "tstzspan" _parse_function = tstzspan_in - _parse_value_function = lambda x: pg_timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) + _parse_value_function = lambda x: timestamptz_in(x, -1) if isinstance(x, str) else datetime_to_timestamptz(x) _make_function = tstzspan_make # ------------------------- Constructors ---------------------------------- @@ -505,13 +505,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspanset import TsTzSpanSet if isinstance(other, datetime): - return timedelta(seconds=distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_span_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspan_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspan_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspan(other._inner, self._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(other._inner, self._inner)) elif isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/collections/time/tstzspanset.py b/pymeos/collections/time/tstzspanset.py index 9e5f10fb..0b824080 100644 --- a/pymeos/collections/time/tstzspanset.py +++ b/pymeos/collections/time/tstzspanset.py @@ -631,13 +631,13 @@ def distance(self, other: Time | Box | Temporal) -> timedelta: from .tstzspan import TsTzSpan if isinstance(other, datetime): - return timedelta(seconds=distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) + return interval_to_timedelta(distance_spanset_timestamptz(self._inner, datetime_to_timestamptz(other))) elif isinstance(other, TsTzSet): return self.distance(other.to_spanset()) elif isinstance(other, TsTzSpan): - return timedelta(seconds=distance_tstzspanset_tstzspan(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspan(self._inner, other._inner)) elif isinstance(other, TsTzSpanSet): - return timedelta(seconds=distance_tstzspanset_tstzspanset(self._inner, other._inner)) + return interval_to_timedelta(distance_tstzspanset_tstzspanset(self._inner, other._inner)) if isinstance(other, Temporal): return self.distance(other.tstzspan()) elif isinstance(other, get_args(Box)): diff --git a/pymeos/main/tfloat.py b/pymeos/main/tfloat.py index 242b3513..acf659da 100644 --- a/pymeos/main/tfloat.py +++ b/pymeos/main/tfloat.py @@ -896,7 +896,7 @@ def value_split(self, size: float, start: float | None = 0) -> list[Temporal]: MEOS Functions: tfloat_value_split """ - fragments, _, count = tfloat_value_split(self._inner, size, start) + fragments, _, count = tfloat_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -924,15 +924,15 @@ def value_time_split( tfloat_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, _, _, count = tfloat_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tint.py b/pymeos/main/tint.py index 658e457e..7d218c3b 100644 --- a/pymeos/main/tint.py +++ b/pymeos/main/tint.py @@ -833,7 +833,7 @@ def value_split(self, size: int, start: int | None = 0) -> list[TInt]: MEOS Functions: tint_value_split """ - fragments, values, count = tint_value_split(self._inner, size, start) + fragments, values, count = tint_value_split(self._inner, size, start, border_inc=True) return [Temporal._factory(fragments[i]) for i in range(count)] def value_time_split( @@ -861,15 +861,15 @@ def value_time_split( tint_value_time_split """ if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + tiles, _, _, count = tint_value_time_split(self._inner, value_size, dt, value_start, st, border_inc=True) return [Temporal._factory(tiles[i]) for i in range(count)] # ------------------------- Database Operations --------------------------- diff --git a/pymeos/main/tnumber.py b/pymeos/main/tnumber.py index c8e33e42..854db1c9 100644 --- a/pymeos/main/tnumber.py +++ b/pymeos/main/tnumber.py @@ -401,17 +401,17 @@ def mul(self, other: int | float | TNumber) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_tint_int(self._inner, other) + result = mul_tint_int(self._inner, other) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_tfloat_float(self._inner, float(other)) + result = mul_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = mult_tnumber_tnumber(self._inner, other._inner) + result = mul_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -428,15 +428,15 @@ def rmul(self, other: int | float) -> TNumber: A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ from .tfloat import TFloat from .tint import TInt if isinstance(self, TInt) and isinstance(other, int): - result = mult_int_tint(other, self._inner) + result = mul_int_tint(other, self._inner) elif isinstance(self, TFloat) and isinstance(other, (int, float)): - result = mult_float_tfloat(float(other), self._inner) + result = mul_float_tfloat(float(other), self._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -566,7 +566,7 @@ def __mul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_tint_int, mult_tfloat_float, mult_tnumber_tnumber + mul_tint_int, mul_tfloat_float, mul_tnumber_tnumber """ return self.mul(other) @@ -582,7 +582,7 @@ def __rmul__(self, other): A new temporal object of the same subtype as `self`. MEOS Functions: - mult_int_tint, mult_float_tfloat + mul_int_tint, mul_float_tfloat """ return self.rmul(other) @@ -660,14 +660,17 @@ def distance(self, other: int | float | TNumber) -> TFloat: A :class:`TFloat` with the distance between `self` and `other`. MEOS Functions: - distance_tfloat_float, distance_tnumber_tnumber + tdistance_tint_int, tdistance_tfloat_float, tdistance_tnumber_tnumber """ - if isinstance(other, int): - result = distance_tfloat_float(self._inner, float(other)) - elif isinstance(other, float): - result = distance_tfloat_float(self._inner, other) + from .tfloat import TFloat + from .tint import TInt + + if isinstance(self, TInt) and isinstance(other, int): + result = tdistance_tint_int(self._inner, other) + elif isinstance(self, TFloat) and isinstance(other, (int, float)): + result = tdistance_tfloat_float(self._inner, float(other)) elif isinstance(other, TNumber): - result = distance_tnumber_tnumber(self._inner, other._inner) + result = tdistance_tnumber_tnumber(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/tpoint.py b/pymeos/main/tpoint.py index 9ae1441b..bfa01329 100644 --- a/pymeos/main/tpoint.py +++ b/pymeos/main/tpoint.py @@ -235,7 +235,7 @@ def length(self) -> float: MEOS Functions: tpoint_length """ - return tpoint_length(self._inner) + return tpoint_length(self._inner, spheroid=True) def cumulative_length(self) -> TFloat: """ @@ -247,7 +247,7 @@ def cumulative_length(self) -> TFloat: MEOS Functions: tpoint_cumulative_length """ - result = tpoint_cumulative_length(self._inner) + result = tpoint_cumulative_length(self._inner, spheroid=True) return Temporal._factory(result) def speed(self) -> TFloat: @@ -260,7 +260,7 @@ def speed(self) -> TFloat: MEOS Functions: tpoint_speed """ - result = tpoint_speed(self._inner) + result = tpoint_speed(self._inner, spheroid=True) return Temporal._factory(result) def x(self) -> TF: @@ -787,11 +787,11 @@ def is_ever_within_distance(self, other: shpb.BaseGeometry | TPoint | STBox, dis if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = edwithin_tgeo_geo(self._inner, gs, distance) + result = edwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance) + result = edwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TPoint): - result = edwithin_tgeo_tgeo(self._inner, other._inner, distance) + result = edwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return result == 1 @@ -866,10 +866,10 @@ def is_spatially_contained_in(self, container: shpb.BaseGeometry | STBox) -> TBo if isinstance(container, shpb.BaseGeometry): gs = geo_to_gserialized(container, isinstance(self, TGeogPoint)) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) elif isinstance(container, STBox): gs = stbox_to_geo(container._inner) - result = tcontains_geo_tgeo(gs, self._inner, False, False) + result = tcontains_geo_tgeo(gs, self._inner) else: raise TypeError(f"Operation not supported with type {container.__class__}") return Temporal._factory(result) @@ -892,11 +892,11 @@ def distance(self, other: shpb.BaseGeometry | TPoint | STBox) -> TFloat: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdistance_tgeo_geo(self._inner, gs) + result = tdistance_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner)) + result = tdistance_tgeo_geo(self._inner, stbox_to_geo(other._inner), spheroid=True) elif isinstance(other, TPoint): - result = tdistance_tgeo_tgeo(self._inner, other._inner) + result = tdistance_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -918,11 +918,11 @@ def nearest_approach_distance(self, other: shpb.BaseGeometry | STBox | TPoint) - if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - return nad_tgeo_geo(self._inner, gs) + return nad_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, STBox): - return nad_tgeo_stbox(self._inner, other._inner) + return nad_tgeo_stbox(self._inner, other._inner, spheroid=True) elif isinstance(other, TPoint): - return nad_tgeo_tgeo(self._inner, other._inner) + return nad_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") @@ -941,9 +941,9 @@ def nearest_approach_instant(self, other: shpb.BaseGeometry | TPoint) -> TI: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = nai_tgeo_geo(self._inner, gs) + result = nai_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = nai_tgeo_tgeo(self._inner, other._inner) + result = nai_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -964,9 +964,9 @@ def shortest_line(self, other: shpb.BaseGeometry | TPoint) -> shpb.BaseGeometry: """ if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = shortestline_tgeo_geo(self._inner, gs) + result = shortestline_tgeo_geo(self._inner, gs, spheroid=True) elif isinstance(other, TPoint): - result = shortestline_tgeo_tgeo(self._inner, other._inner) + result = shortestline_tgeo_tgeo(self._inner, other._inner, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return gserialized_to_shapely_geometry(result, 10) @@ -1086,19 +1086,19 @@ def space_time_split( """ ysz = ysize if ysize is not None else xsize zsz = zsize if zsize is not None else xsize - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) gs = ( geo_to_gserialized(origin, isinstance(self, TGeogPoint)) if origin is not None else (geog_in("Point(0 0 0)", -1) if isinstance(self, TGeogPoint) else geom_in("Point(0 0 0)", -1)) ) if time_start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: st = ( datetime_to_timestamptz(time_start) if isinstance(time_start, datetime) - else pg_timestamptz_in(time_start, -1) + else timestamptz_in(time_start, -1) ) fragments, points, times, count = tgeo_space_time_split( self._inner, xsize, ysz, zsz, dt, gs, st, bitmatrix, include_border @@ -1365,11 +1365,11 @@ def disjoint(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdisjoint_tgeo_geo(self._inner, gs, False, False) + result = tdisjoint_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tdisjoint_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tdisjoint_tgeo_tgeo(self._inner, other._inner, False, False) + result = tdisjoint_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1391,11 +1391,11 @@ def intersects(self, other: shpb.BaseGeometry | STBox | TPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tintersects_tgeo_geo(self._inner, gs, False, False) + result = tintersects_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = tintersects_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TPoint): - result = tintersects_tgeo_tgeo(self._inner, other._inner, False, False) + result = tintersects_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1417,11 +1417,11 @@ def touches(self, other: shpb.BaseGeometry | STBox | TGeomPoint) -> TBool: if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = ttouches_tgeo_geo(self._inner, gs, False, False) + result = ttouches_tgeo_geo(self._inner, gs) elif isinstance(other, STBox): - result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner), False, False) + result = ttouches_tgeo_geo(self._inner, stbox_to_geo(other._inner)) elif isinstance(other, TGeomPoint): - result = ttouches_tgeo_tgeo(self._inner, other._inner, False, False) + result = ttouches_tgeo_tgeo(self._inner, other._inner) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) @@ -1444,11 +1444,11 @@ def within_distance(self, other: shpb.BaseGeometry | TGeomPoint | STBox, distanc if isinstance(other, shpb.BaseGeometry): gs = geo_to_gserialized(other, isinstance(self, TGeogPoint)) - result = tdwithin_tgeo_geo(self._inner, gs, distance, False, False) + result = tdwithin_tgeo_geo(self._inner, gs, distance, spheroid=True) elif isinstance(other, STBox): - result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, False, False) + result = tdwithin_tgeo_geo(self._inner, stbox_to_geo(other._inner), distance, spheroid=True) elif isinstance(other, TGeomPoint): - result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, False, False) + result = tdwithin_tgeo_tgeo(self._inner, other._inner, distance, spheroid=True) else: raise TypeError(f"Operation not supported with type {other.__class__}") return Temporal._factory(result) diff --git a/pymeos/main/ttext.py b/pymeos/main/ttext.py index e956e732..2dfd521b 100644 --- a/pymeos/main/ttext.py +++ b/pymeos/main/ttext.py @@ -143,7 +143,7 @@ def value_set(self) -> set[str]: ttext_values """ values, count = ttext_values(self._inner) - return {text2cstring(values[i]) for i in range(count)} + return {text_to_cstring(values[i]) for i in range(count)} def min_value(self) -> str: """ @@ -233,7 +233,7 @@ def value_at_timestamp(self, timestamp: datetime) -> str: ttext_value_at_timestamp """ result = ttext_value_at_timestamptz(self._inner, datetime_to_timestamptz(timestamp), True) - return text2cstring(result[0]) + return text_to_cstring(result[0]) # ------------------------- Ever and Always Comparisons ------------------- def always_less(self, value: str | TText) -> bool: diff --git a/pymeos/portable.py b/pymeos/portable.py new file mode 100644 index 00000000..cf7dd686 --- /dev/null +++ b/pymeos/portable.py @@ -0,0 +1,342 @@ +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# ============================================================================ +# GENERATED by tools/portable_aliases/generate.py -- DO NOT EDIT. +# Regenerate: python3 tools/portable_aliases/generate.py --headers +# ============================================================================ +# +# The portable bare-name dialect (RFC #920; canonical contract: +# MobilityDB/MEOS-API meta/portable-aliases.json; native in MobilityDB via +# the 1303-alias PR #1075). Every binding exposes the IDENTICAL bare names +# from that one mapping, type-agnostically across all six temporal type +# families (temporal, geo, cbuffer, npoint, pose, rgeo). +# +# Each bare name dispatches, by the runtime C type of its arguments, to the +# EXACT pymeos_cffi function the operator is backed by -- the same native +# call the type-qualified API uses. Nothing is reimplemented: every bare +# name is identical to its operator by construction. +"""Portable bare-name dialect for PyMEOS (generated -- see module comment).""" +from __future__ import annotations + +import datetime as _dt + +# Resolved at call time so importing this module never requires the compiled +# binding to be present (e.g. docs / parity checks). +_cffi = None + + +def _pymeos_cffi(): + global _cffi + if _cffi is None: + import pymeos_cffi as _c + + _cffi = _c + return _cffi + + +def _unwrap(a): + return a._inner if hasattr(a, "_inner") else a + + +def _arg_token(a): + """Runtime C base type of an argument, mirroring pymeos.factory's cdata + field-sniffing (no ffi object needed) plus Python scalars.""" + if isinstance(a, bool): + return "bool" + if isinstance(a, int): + return "int" + if isinstance(a, float): + return "float" + if isinstance(a, str): + return "text" + if isinstance(a, _dt.datetime): + return "timestamptz" + if isinstance(a, _dt.date): + return "date" + if isinstance(a, _dt.timedelta): + return "interval" + o = _unwrap(a) + for attr, tok in ( + ("temptype", "Temporal"), # every temporal subtype (6 families) + ("spansettype", "SpanSet"), + ("spantype", "Span"), + ("settype", "Set"), + ): + if hasattr(o, attr): + return tok + # Boxes / geo / spatial base types: distinguishing struct fields. + for attr, tok in ( + ("srid", "STBox"), # STBox carries an srid; TBox does not + ("period", "TBox"), + ("gflags", "GSERIALIZED"), + ("rid", "Npoint"), + ("radius", "Cbuffer"), + ): + if hasattr(o, attr): + return tok + return type(a).__name__ + + +def _wrap(res): + """Re-wrap MEOS-object results with the existing PyMEOS factories; pass + scalars (bool/float/int/str) and None straight through.""" + if res is None or isinstance(res, (bool, int, float, str, bytes)): + return res + from .factory import _TemporalFactory, _CollectionFactory + + for fn in (_TemporalFactory.create_temporal, + _CollectionFactory.create_collection): + try: + return fn(res) + except Exception: + pass + return res + + +def _dispatch(name, table, args): + key = tuple(_arg_token(a) for a in args) + sym = table.get(key) + if sym is None and len(key) == 2: # symmetric fallback + sym = table.get((key[1], key[0])) + if sym is None: + raise TypeError( + f"{name}(): no portable overload for argument types {key}; " + f"supported: {sorted(table)}" + ) + fn = getattr(_pymeos_cffi(), sym) + return _wrap(fn(*[_unwrap(a) for a in args])) + +__all__ = ['above', 'adjacent', 'after', 'back', 'before', 'below', 'contained', 'contains', 'front', 'left', 'nearestApproachDistance', 'overabove', 'overafter', 'overback', 'overbefore', 'overbelow', 'overfront', 'overlaps', 'overleft', 'overright', 'right', 'same', 'tdistance', 'teq', 'tge', 'tgt', 'tle', 'tlt', 'tne'] + + +_T_above = {('STBox', 'STBox'): 'above_stbox_stbox', ('STBox', 'Temporal'): 'above_stbox_tspatial', ('Temporal', 'STBox'): 'above_tspatial_stbox', ('Temporal', 'Temporal'): 'above_tspatial_tspatial'} + + +def above(*args): + """Portable ``|>>`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('above', _T_above, args) + + +_T_adjacent = {('Span', 'Temporal'): 'adjacent_tstzspan_temporal', ('Span', 'bigint'): 'adjacent_span_bigint', ('Span', 'date'): 'adjacent_span_date', ('Span', 'float'): 'adjacent_span_float', ('Span', 'int'): 'adjacent_span_int', ('Span', 'Span'): 'adjacent_span_span', ('Span', 'SpanSet'): 'adjacent_span_spanset', ('Span', 'timestamptz'): 'adjacent_span_timestamptz', ('Span', 'Datum'): 'adjacent_span_value', ('SpanSet', 'bigint'): 'adjacent_spanset_bigint', ('SpanSet', 'date'): 'adjacent_spanset_date', ('SpanSet', 'float'): 'adjacent_spanset_float', ('SpanSet', 'int'): 'adjacent_spanset_int', ('SpanSet', 'Span'): 'adjacent_spanset_span', ('SpanSet', 'SpanSet'): 'adjacent_spanset_spanset', ('SpanSet', 'timestamptz'): 'adjacent_spanset_timestamptz', ('SpanSet', 'Datum'): 'adjacent_spanset_value', ('STBox', 'STBox'): 'adjacent_stbox_stbox', ('STBox', 'Temporal'): 'adjacent_stbox_tspatial', ('TBox', 'TBox'): 'adjacent_tbox_tbox', ('TBox', 'Temporal'): 'adjacent_tbox_tnumber', ('Temporal', 'Temporal'): 'adjacent_tspatial_tspatial', ('Temporal', 'Span'): 'adjacent_tnumber_numspan', ('Temporal', 'TBox'): 'adjacent_tnumber_tbox', ('Temporal', 'STBox'): 'adjacent_tspatial_stbox', ('Datum', 'SpanSet'): 'adjacent_value_spanset'} + + +def adjacent(*args): + """Portable ``-|-`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('adjacent', _T_adjacent, args) + + +_T_after = {('date', 'Set'): 'after_date_set', ('date', 'Span'): 'after_date_span', ('date', 'SpanSet'): 'after_date_spanset', ('Set', 'date'): 'after_set_date', ('Set', 'timestamptz'): 'after_set_timestamptz', ('Span', 'date'): 'after_span_date', ('Span', 'timestamptz'): 'after_span_timestamptz', ('SpanSet', 'date'): 'after_spanset_date', ('SpanSet', 'timestamptz'): 'after_spanset_timestamptz', ('STBox', 'STBox'): 'after_stbox_stbox', ('STBox', 'Temporal'): 'after_stbox_tspatial', ('TBox', 'TBox'): 'after_tbox_tbox', ('TBox', 'Temporal'): 'after_tbox_tnumber', ('Temporal', 'Temporal'): 'after_tspatial_tspatial', ('Temporal', 'Span'): 'after_temporal_tstzspan', ('timestamptz', 'Set'): 'after_timestamptz_set', ('timestamptz', 'Span'): 'after_timestamptz_span', ('timestamptz', 'SpanSet'): 'after_timestamptz_spanset', ('Temporal', 'TBox'): 'after_tnumber_tbox', ('Temporal', 'STBox'): 'after_tspatial_stbox', ('Span', 'Temporal'): 'after_tstzspan_temporal'} + + +def after(*args): + """Portable ``#>>`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('after', _T_after, args) + + +_T_back = {('STBox', 'STBox'): 'back_stbox_stbox', ('STBox', 'Temporal'): 'back_stbox_tspatial', ('Temporal', 'STBox'): 'back_tspatial_stbox', ('Temporal', 'Temporal'): 'back_tspatial_tspatial'} + + +def back(*args): + """Portable ``/>>`` (spaceZ) -> pymeos_cffi backing call.""" + return _dispatch('back', _T_back, args) + + +_T_before = {('date', 'Set'): 'before_date_set', ('date', 'Span'): 'before_date_span', ('date', 'SpanSet'): 'before_date_spanset', ('Set', 'date'): 'before_set_date', ('Set', 'timestamptz'): 'before_set_timestamptz', ('Span', 'date'): 'before_span_date', ('Span', 'timestamptz'): 'before_span_timestamptz', ('SpanSet', 'date'): 'before_spanset_date', ('SpanSet', 'timestamptz'): 'before_spanset_timestamptz', ('STBox', 'STBox'): 'before_stbox_stbox', ('STBox', 'Temporal'): 'before_stbox_tspatial', ('TBox', 'TBox'): 'before_tbox_tbox', ('TBox', 'Temporal'): 'before_tbox_tnumber', ('Temporal', 'Temporal'): 'before_tspatial_tspatial', ('Temporal', 'Span'): 'before_temporal_tstzspan', ('timestamptz', 'Set'): 'before_timestamptz_set', ('timestamptz', 'Span'): 'before_timestamptz_span', ('timestamptz', 'SpanSet'): 'before_timestamptz_spanset', ('Temporal', 'TBox'): 'before_tnumber_tbox', ('Temporal', 'STBox'): 'before_tspatial_stbox', ('Span', 'Temporal'): 'before_tstzspan_temporal'} + + +def before(*args): + """Portable ``<<#`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('before', _T_before, args) + + +_T_below = {('STBox', 'STBox'): 'below_stbox_stbox', ('STBox', 'Temporal'): 'below_stbox_tspatial', ('Temporal', 'STBox'): 'below_tspatial_stbox', ('Temporal', 'Temporal'): 'below_tspatial_tspatial'} + + +def below(*args): + """Portable ``<<|`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('below', _T_below, args) + + +_T_contained = {('bigint', 'Set'): 'contained_bigint_set', ('bigint', 'Span'): 'contained_bigint_span', ('bigint', 'SpanSet'): 'contained_bigint_spanset', ('Cbuffer', 'Set'): 'contained_cbuffer_set', ('date', 'Set'): 'contained_date_set', ('date', 'Span'): 'contained_date_span', ('date', 'SpanSet'): 'contained_date_spanset', ('float', 'Set'): 'contained_float_set', ('float', 'Span'): 'contained_float_span', ('float', 'SpanSet'): 'contained_float_spanset', ('GSERIALIZED', 'Set'): 'contained_geo_set', ('int', 'Set'): 'contained_int_set', ('int', 'Span'): 'contained_int_span', ('int', 'SpanSet'): 'contained_int_spanset', ('Jsonb', 'Set'): 'contained_jsonb_set', ('Npoint', 'Set'): 'contained_npoint_set', ('Span', 'Temporal'): 'contained_tstzspan_temporal', ('Pose', 'Set'): 'contained_pose_set', ('Set', 'Set'): 'contained_set_set', ('Span', 'Span'): 'contained_span_span', ('Span', 'SpanSet'): 'contained_span_spanset', ('SpanSet', 'Span'): 'contained_spanset_span', ('SpanSet', 'SpanSet'): 'contained_spanset_spanset', ('STBox', 'STBox'): 'contained_stbox_stbox', ('STBox', 'Temporal'): 'contained_stbox_tspatial', ('TBox', 'TBox'): 'contained_tbox_tbox', ('TBox', 'Temporal'): 'contained_tbox_tnumber', ('Temporal', 'Temporal'): 'contained_tspatial_tspatial', ('Temporal', 'Span'): 'contained_tnumber_numspan', ('text', 'Set'): 'contained_text_set', ('timestamptz', 'Set'): 'contained_timestamptz_set', ('timestamptz', 'Span'): 'contained_timestamptz_span', ('timestamptz', 'SpanSet'): 'contained_timestamptz_spanset', ('Temporal', 'TBox'): 'contained_tnumber_tbox', ('Temporal', 'STBox'): 'contained_tspatial_stbox', ('Datum', 'Set'): 'contained_value_set', ('Datum', 'Span'): 'contained_value_span', ('Datum', 'SpanSet'): 'contained_value_spanset'} + + +def contained(*args): + """Portable ``<@`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('contained', _T_contained, args) + + +_T_contains = {('Cbuffer', 'Cbuffer'): 'contains_cbuffer_cbuffer', ('Span', 'Temporal'): 'contains_tstzspan_temporal', ('Set', 'bigint'): 'contains_set_bigint', ('Set', 'Cbuffer'): 'contains_set_cbuffer', ('Set', 'date'): 'contains_set_date', ('Set', 'float'): 'contains_set_float', ('Set', 'GSERIALIZED'): 'contains_set_geo', ('Set', 'int'): 'contains_set_int', ('Set', 'Jsonb'): 'contains_set_jsonb', ('Set', 'Npoint'): 'contains_set_npoint', ('Set', 'Pose'): 'contains_set_pose', ('Set', 'Set'): 'contains_set_set', ('Set', 'text'): 'contains_set_text', ('Set', 'timestamptz'): 'contains_set_timestamptz', ('Set', 'Datum'): 'contains_set_value', ('Span', 'bigint'): 'contains_span_bigint', ('Span', 'date'): 'contains_span_date', ('Span', 'float'): 'contains_span_float', ('Span', 'int'): 'contains_span_int', ('Span', 'Span'): 'contains_span_span', ('Span', 'SpanSet'): 'contains_span_spanset', ('Span', 'timestamptz'): 'contains_span_timestamptz', ('Span', 'Datum'): 'contains_span_value', ('SpanSet', 'bigint'): 'contains_spanset_bigint', ('SpanSet', 'date'): 'contains_spanset_date', ('SpanSet', 'float'): 'contains_spanset_float', ('SpanSet', 'int'): 'contains_spanset_int', ('SpanSet', 'Span'): 'contains_spanset_span', ('SpanSet', 'SpanSet'): 'contains_spanset_spanset', ('SpanSet', 'timestamptz'): 'contains_spanset_timestamptz', ('SpanSet', 'Datum'): 'contains_spanset_value', ('STBox', 'STBox'): 'contains_stbox_stbox', ('STBox', 'Temporal'): 'contains_stbox_tspatial', ('TBox', 'TBox'): 'contains_tbox_tbox', ('TBox', 'Temporal'): 'contains_tbox_tnumber', ('Temporal', 'Temporal'): 'contains_tspatial_tspatial', ('Temporal', 'Span'): 'contains_tnumber_numspan', ('Temporal', 'Jsonb', 'bool'): 'contains_tjsonb_jsonb', ('Temporal', 'TBox'): 'contains_tnumber_tbox', ('Temporal', 'STBox'): 'contains_tspatial_stbox'} + + +def contains(*args): + """Portable ``@>`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('contains', _T_contains, args) + + +_T_front = {('STBox', 'STBox'): 'front_stbox_stbox', ('STBox', 'Temporal'): 'front_stbox_tspatial', ('Temporal', 'STBox'): 'front_tspatial_stbox', ('Temporal', 'Temporal'): 'front_tspatial_tspatial'} + + +def front(*args): + """Portable ``< pymeos_cffi backing call.""" + return _dispatch('front', _T_front, args) + + +_T_left = {('bigint', 'Set'): 'left_bigint_set', ('bigint', 'Span'): 'left_bigint_span', ('bigint', 'SpanSet'): 'left_bigint_spanset', ('float', 'Set'): 'left_float_set', ('float', 'Span'): 'left_float_span', ('float', 'SpanSet'): 'left_float_spanset', ('int', 'Set'): 'left_int_set', ('int', 'Span'): 'left_int_span', ('int', 'SpanSet'): 'left_int_spanset', ('Span', 'Temporal'): 'left_numspan_tnumber', ('Set', 'bigint'): 'left_set_bigint', ('Set', 'float'): 'left_set_float', ('Set', 'int'): 'left_set_int', ('Set', 'Set'): 'left_set_set', ('Set', 'text'): 'left_set_text', ('Set', 'Datum'): 'left_set_value', ('Span', 'bigint'): 'left_span_bigint', ('Span', 'float'): 'left_span_float', ('Span', 'int'): 'left_span_int', ('Span', 'Span'): 'left_span_span', ('Span', 'SpanSet'): 'left_span_spanset', ('Span', 'Datum'): 'left_span_value', ('SpanSet', 'bigint'): 'left_spanset_bigint', ('SpanSet', 'float'): 'left_spanset_float', ('SpanSet', 'int'): 'left_spanset_int', ('SpanSet', 'Span'): 'left_spanset_span', ('SpanSet', 'SpanSet'): 'left_spanset_spanset', ('SpanSet', 'Datum'): 'left_spanset_value', ('STBox', 'STBox'): 'left_stbox_stbox', ('STBox', 'Temporal'): 'left_stbox_tspatial', ('TBox', 'TBox'): 'left_tbox_tbox', ('TBox', 'Temporal'): 'left_tbox_tnumber', ('text', 'Set'): 'left_text_set', ('Temporal', 'Span'): 'left_tnumber_numspan', ('Temporal', 'TBox'): 'left_tnumber_tbox', ('Temporal', 'Temporal'): 'left_tspatial_tspatial', ('Temporal', 'STBox'): 'left_tspatial_stbox', ('Datum', 'Set'): 'left_value_set', ('Datum', 'Span'): 'left_value_span', ('Datum', 'SpanSet'): 'left_value_spanset'} + + +def left(*args): + """Portable ``<<`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('left', _T_left, args) + + +_T_nearestApproachDistance = {('Cbuffer', 'STBox'): 'nad_cbuffer_stbox', ('STBox', 'GSERIALIZED'): 'nad_stbox_geo', ('STBox', 'STBox'): 'nad_stbox_stbox', ('STBox', 'Temporal'): 'nad_stbox_trgeo', ('TBox', 'TBox'): 'nad_tboxint_tboxint', ('Temporal', 'Cbuffer'): 'nad_tcbuffer_cbuffer', ('Temporal', 'GSERIALIZED'): 'nad_trgeo_geo', ('Temporal', 'STBox'): 'nad_trgeo_stbox', ('Temporal', 'Temporal'): 'nad_trgeo_trgeo', ('Temporal', 'float'): 'nad_tfloat_float', ('Temporal', 'TBox'): 'nad_tnumber_tbox', ('Temporal', 'int'): 'nad_tint_int', ('Temporal', 'Npoint'): 'nad_tnpoint_npoint', ('Temporal', 'Datum'): 'nad_tnumber_number', ('Temporal', 'Pose'): 'nad_tpose_pose'} + + +def nearestApproachDistance(*args): + """Portable ``|=|`` (distance) -> pymeos_cffi backing call.""" + return _dispatch('nearestApproachDistance', _T_nearestApproachDistance, args) + + +_T_overabove = {('STBox', 'STBox'): 'overabove_stbox_stbox', ('STBox', 'Temporal'): 'overabove_stbox_tspatial', ('Temporal', 'STBox'): 'overabove_tspatial_stbox', ('Temporal', 'Temporal'): 'overabove_tspatial_tspatial'} + + +def overabove(*args): + """Portable ``|&>`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('overabove', _T_overabove, args) + + +_T_overafter = {('date', 'Set'): 'overafter_date_set', ('date', 'Span'): 'overafter_date_span', ('date', 'SpanSet'): 'overafter_date_spanset', ('Set', 'date'): 'overafter_set_date', ('Set', 'timestamptz'): 'overafter_set_timestamptz', ('Span', 'date'): 'overafter_span_date', ('Span', 'timestamptz'): 'overafter_span_timestamptz', ('SpanSet', 'date'): 'overafter_spanset_date', ('SpanSet', 'timestamptz'): 'overafter_spanset_timestamptz', ('STBox', 'STBox'): 'overafter_stbox_stbox', ('STBox', 'Temporal'): 'overafter_stbox_tspatial', ('TBox', 'TBox'): 'overafter_tbox_tbox', ('TBox', 'Temporal'): 'overafter_tbox_tnumber', ('Temporal', 'Temporal'): 'overafter_tspatial_tspatial', ('Temporal', 'Span'): 'overafter_temporal_tstzspan', ('timestamptz', 'Set'): 'overafter_timestamptz_set', ('timestamptz', 'Span'): 'overafter_timestamptz_span', ('timestamptz', 'SpanSet'): 'overafter_timestamptz_spanset', ('Temporal', 'TBox'): 'overafter_tnumber_tbox', ('Temporal', 'STBox'): 'overafter_tspatial_stbox', ('Span', 'Temporal'): 'overafter_tstzspan_temporal'} + + +def overafter(*args): + """Portable ``#&>`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('overafter', _T_overafter, args) + + +_T_overback = {('STBox', 'STBox'): 'overback_stbox_stbox', ('STBox', 'Temporal'): 'overback_stbox_tspatial', ('Temporal', 'STBox'): 'overback_tspatial_stbox', ('Temporal', 'Temporal'): 'overback_tspatial_tspatial'} + + +def overback(*args): + """Portable ``/&>`` (spaceZ) -> pymeos_cffi backing call.""" + return _dispatch('overback', _T_overback, args) + + +_T_overbefore = {('date', 'Set'): 'overbefore_date_set', ('date', 'Span'): 'overbefore_date_span', ('date', 'SpanSet'): 'overbefore_date_spanset', ('Set', 'date'): 'overbefore_set_date', ('Set', 'timestamptz'): 'overbefore_set_timestamptz', ('Span', 'date'): 'overbefore_span_date', ('Span', 'timestamptz'): 'overbefore_span_timestamptz', ('SpanSet', 'date'): 'overbefore_spanset_date', ('SpanSet', 'timestamptz'): 'overbefore_spanset_timestamptz', ('STBox', 'STBox'): 'overbefore_stbox_stbox', ('STBox', 'Temporal'): 'overbefore_stbox_tspatial', ('TBox', 'TBox'): 'overbefore_tbox_tbox', ('TBox', 'Temporal'): 'overbefore_tbox_tnumber', ('Temporal', 'Temporal'): 'overbefore_tspatial_tspatial', ('Temporal', 'Span'): 'overbefore_temporal_tstzspan', ('timestamptz', 'Set'): 'overbefore_timestamptz_set', ('timestamptz', 'Span'): 'overbefore_timestamptz_span', ('timestamptz', 'SpanSet'): 'overbefore_timestamptz_spanset', ('Temporal', 'TBox'): 'overbefore_tnumber_tbox', ('Temporal', 'STBox'): 'overbefore_tspatial_stbox', ('Span', 'Temporal'): 'overbefore_tstzspan_temporal'} + + +def overbefore(*args): + """Portable ``&<#`` (timePosition) -> pymeos_cffi backing call.""" + return _dispatch('overbefore', _T_overbefore, args) + + +_T_overbelow = {('STBox', 'STBox'): 'overbelow_stbox_stbox', ('STBox', 'Temporal'): 'overbelow_stbox_tspatial', ('Temporal', 'STBox'): 'overbelow_tspatial_stbox', ('Temporal', 'Temporal'): 'overbelow_tspatial_tspatial'} + + +def overbelow(*args): + """Portable ``&<|`` (spaceY) -> pymeos_cffi backing call.""" + return _dispatch('overbelow', _T_overbelow, args) + + +_T_overfront = {('STBox', 'STBox'): 'overfront_stbox_stbox', ('STBox', 'Temporal'): 'overfront_stbox_tspatial', ('Temporal', 'STBox'): 'overfront_tspatial_stbox', ('Temporal', 'Temporal'): 'overfront_tspatial_tspatial'} + + +def overfront(*args): + """Portable ``& pymeos_cffi backing call.""" + return _dispatch('overfront', _T_overfront, args) + + +_T_overlaps = {('Span', 'Temporal'): 'overlaps_tstzspan_temporal', ('Set', 'Set'): 'overlaps_set_set', ('Span', 'Span'): 'overlaps_span_span', ('Span', 'SpanSet'): 'overlaps_span_spanset', ('SpanSet', 'Span'): 'overlaps_spanset_span', ('SpanSet', 'SpanSet'): 'overlaps_spanset_spanset', ('STBox', 'STBox'): 'overlaps_stbox_stbox', ('STBox', 'Temporal'): 'overlaps_stbox_tspatial', ('TBox', 'TBox'): 'overlaps_tbox_tbox', ('TBox', 'Temporal'): 'overlaps_tbox_tnumber', ('Temporal', 'Temporal'): 'overlaps_tspatial_tspatial', ('Temporal', 'Span'): 'overlaps_tnumber_numspan', ('Temporal', 'TBox'): 'overlaps_tnumber_tbox', ('Temporal', 'STBox'): 'overlaps_tspatial_stbox'} + + +def overlaps(*args): + """Portable ``&&`` (topology) -> pymeos_cffi backing call.""" + return _dispatch('overlaps', _T_overlaps, args) + + +_T_overleft = {('bigint', 'Set'): 'overleft_bigint_set', ('bigint', 'Span'): 'overleft_bigint_span', ('bigint', 'SpanSet'): 'overleft_bigint_spanset', ('float', 'Set'): 'overleft_float_set', ('float', 'Span'): 'overleft_float_span', ('float', 'SpanSet'): 'overleft_float_spanset', ('int', 'Set'): 'overleft_int_set', ('int', 'Span'): 'overleft_int_span', ('int', 'SpanSet'): 'overleft_int_spanset', ('Span', 'Temporal'): 'overleft_numspan_tnumber', ('Set', 'bigint'): 'overleft_set_bigint', ('Set', 'float'): 'overleft_set_float', ('Set', 'int'): 'overleft_set_int', ('Set', 'Set'): 'overleft_set_set', ('Set', 'text'): 'overleft_set_text', ('Set', 'Datum'): 'overleft_set_value', ('Span', 'bigint'): 'overleft_span_bigint', ('Span', 'float'): 'overleft_span_float', ('Span', 'int'): 'overleft_span_int', ('Span', 'Span'): 'overleft_span_span', ('Span', 'SpanSet'): 'overleft_span_spanset', ('Span', 'Datum'): 'overleft_span_value', ('SpanSet', 'bigint'): 'overleft_spanset_bigint', ('SpanSet', 'float'): 'overleft_spanset_float', ('SpanSet', 'int'): 'overleft_spanset_int', ('SpanSet', 'Span'): 'overleft_spanset_span', ('SpanSet', 'SpanSet'): 'overleft_spanset_spanset', ('SpanSet', 'Datum'): 'overleft_spanset_value', ('STBox', 'STBox'): 'overleft_stbox_stbox', ('STBox', 'Temporal'): 'overleft_stbox_tspatial', ('TBox', 'TBox'): 'overleft_tbox_tbox', ('TBox', 'Temporal'): 'overleft_tbox_tnumber', ('text', 'Set'): 'overleft_text_set', ('Temporal', 'Span'): 'overleft_tnumber_numspan', ('Temporal', 'TBox'): 'overleft_tnumber_tbox', ('Temporal', 'Temporal'): 'overleft_tspatial_tspatial', ('Temporal', 'STBox'): 'overleft_tspatial_stbox', ('Datum', 'Set'): 'overleft_value_set', ('Datum', 'Span'): 'overleft_value_span', ('Datum', 'SpanSet'): 'overleft_value_spanset'} + + +def overleft(*args): + """Portable ``&<`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('overleft', _T_overleft, args) + + +_T_overright = {('bigint', 'Set'): 'overright_bigint_set', ('bigint', 'Span'): 'overright_bigint_span', ('bigint', 'SpanSet'): 'overright_bigint_spanset', ('float', 'Set'): 'overright_float_set', ('float', 'Span'): 'overright_float_span', ('float', 'SpanSet'): 'overright_float_spanset', ('int', 'Set'): 'overright_int_set', ('int', 'Span'): 'overright_int_span', ('int', 'SpanSet'): 'overright_int_spanset', ('Span', 'Temporal'): 'overright_numspan_tnumber', ('Set', 'bigint'): 'overright_set_bigint', ('Set', 'float'): 'overright_set_float', ('Set', 'int'): 'overright_set_int', ('Set', 'Set'): 'overright_set_set', ('Set', 'text'): 'overright_set_text', ('Set', 'Datum'): 'overright_set_value', ('Span', 'bigint'): 'overright_span_bigint', ('Span', 'float'): 'overright_span_float', ('Span', 'int'): 'overright_span_int', ('Span', 'Span'): 'overright_span_span', ('Span', 'SpanSet'): 'overright_span_spanset', ('Span', 'Datum'): 'overright_span_value', ('SpanSet', 'bigint'): 'overright_spanset_bigint', ('SpanSet', 'float'): 'overright_spanset_float', ('SpanSet', 'int'): 'overright_spanset_int', ('SpanSet', 'Span'): 'overright_spanset_span', ('SpanSet', 'SpanSet'): 'overright_spanset_spanset', ('SpanSet', 'Datum'): 'overright_spanset_value', ('STBox', 'STBox'): 'overright_stbox_stbox', ('STBox', 'Temporal'): 'overright_stbox_tspatial', ('TBox', 'TBox'): 'overright_tbox_tbox', ('TBox', 'Temporal'): 'overright_tbox_tnumber', ('text', 'Set'): 'overright_text_set', ('Temporal', 'Span'): 'overright_tnumber_numspan', ('Temporal', 'TBox'): 'overright_tnumber_tbox', ('Temporal', 'Temporal'): 'overright_tspatial_tspatial', ('Temporal', 'STBox'): 'overright_tspatial_stbox', ('Datum', 'Set'): 'overright_value_set', ('Datum', 'Span'): 'overright_value_span', ('Datum', 'SpanSet'): 'overright_value_spanset'} + + +def overright(*args): + """Portable ``&>`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('overright', _T_overright, args) + + +_T_right = {('bigint', 'Set'): 'right_bigint_set', ('bigint', 'Span'): 'right_bigint_span', ('bigint', 'SpanSet'): 'right_bigint_spanset', ('float', 'Set'): 'right_float_set', ('float', 'Span'): 'right_float_span', ('float', 'SpanSet'): 'right_float_spanset', ('int', 'Set'): 'right_int_set', ('int', 'Span'): 'right_int_span', ('int', 'SpanSet'): 'right_int_spanset', ('Span', 'Temporal'): 'right_numspan_tnumber', ('Set', 'bigint'): 'right_set_bigint', ('Set', 'float'): 'right_set_float', ('Set', 'int'): 'right_set_int', ('Set', 'Set'): 'right_set_set', ('Set', 'text'): 'right_set_text', ('Set', 'Datum'): 'right_set_value', ('Span', 'bigint'): 'right_span_bigint', ('Span', 'float'): 'right_span_float', ('Span', 'int'): 'right_span_int', ('Span', 'Span'): 'right_span_span', ('Span', 'SpanSet'): 'right_span_spanset', ('Span', 'Datum'): 'right_span_value', ('SpanSet', 'bigint'): 'right_spanset_bigint', ('SpanSet', 'float'): 'right_spanset_float', ('SpanSet', 'int'): 'right_spanset_int', ('SpanSet', 'Span'): 'right_spanset_span', ('SpanSet', 'SpanSet'): 'right_spanset_spanset', ('SpanSet', 'Datum'): 'right_spanset_value', ('STBox', 'STBox'): 'right_stbox_stbox', ('STBox', 'Temporal'): 'right_stbox_tspatial', ('TBox', 'TBox'): 'right_tbox_tbox', ('TBox', 'Temporal'): 'right_tbox_tnumber', ('text', 'Set'): 'right_text_set', ('Temporal', 'Span'): 'right_tnumber_numspan', ('Temporal', 'TBox'): 'right_tnumber_tbox', ('Temporal', 'Temporal'): 'right_tspatial_tspatial', ('Temporal', 'STBox'): 'right_tspatial_stbox', ('Datum', 'Set'): 'right_value_set', ('Datum', 'Span'): 'right_value_span', ('Datum', 'SpanSet'): 'right_value_spanset'} + + +def right(*args): + """Portable ``>>`` (spaceX) -> pymeos_cffi backing call.""" + return _dispatch('right', _T_right, args) + + +_T_same = {('Span', 'Temporal'): 'same_tstzspan_temporal', ('STBox', 'STBox'): 'same_stbox_stbox', ('STBox', 'Temporal'): 'same_stbox_tspatial', ('TBox', 'TBox'): 'same_tbox_tbox', ('TBox', 'Temporal'): 'same_tbox_tnumber', ('Temporal', 'Temporal'): 'same_tspatial_tspatial', ('Temporal', 'Span'): 'same_tnumber_numspan', ('Temporal', 'TBox'): 'same_tnumber_tbox', ('Temporal', 'STBox'): 'same_tspatial_stbox'} + + +def same(*args): + """Portable ``~=`` (same) -> pymeos_cffi backing call.""" + return _dispatch('same', _T_same, args) + + +_T_tdistance = {('Temporal', 'Cbuffer'): 'tdistance_tcbuffer_cbuffer', ('Temporal', 'GSERIALIZED'): 'tdistance_trgeo_geo', ('Temporal', 'Temporal'): 'tdistance_trgeo_trgeo', ('Temporal', 'float'): 'tdistance_tfloat_float', ('Temporal', 'int'): 'tdistance_tint_int', ('Temporal', 'Npoint'): 'tdistance_tnpoint_npoint', ('Temporal', 'Datum'): 'tdistance_tnumber_number', ('Temporal', 'Pose'): 'tdistance_tpose_pose'} + + +def tdistance(*args): + """Portable ``<->`` (distance) -> pymeos_cffi backing call.""" + return _dispatch('tdistance', _T_tdistance, args) + + +_T_teq = {('bool', 'Temporal'): 'teq_bool_tbool', ('Cbuffer', 'Temporal'): 'teq_cbuffer_tcbuffer', ('float', 'Temporal'): 'teq_float_tfloat', ('GSERIALIZED', 'Temporal'): 'teq_geo_trgeo', ('int', 'Temporal'): 'teq_int_tint', ('Jsonb', 'Temporal'): 'teq_jsonb_tjsonb', ('Pose', 'Temporal'): 'teq_pose_tpose', ('Temporal', 'bool'): 'teq_tbool_bool', ('Temporal', 'Cbuffer'): 'teq_tcbuffer_cbuffer', ('Temporal', 'Temporal'): 'teq_temporal_temporal', ('text', 'Temporal'): 'teq_text_ttext', ('Temporal', 'float'): 'teq_tfloat_float', ('Temporal', 'GSERIALIZED'): 'teq_trgeo_geo', ('Temporal', 'int'): 'teq_tint_int', ('Temporal', 'Jsonb'): 'teq_tjsonb_jsonb', ('Temporal', 'Npoint'): 'teq_tnpoint_npoint', ('Temporal', 'Pose'): 'teq_tpose_pose', ('Temporal', 'text'): 'teq_ttext_text'} + + +def teq(*args): + """Portable ``#=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('teq', _T_teq, args) + + +_T_tge = {('float', 'Temporal'): 'tge_float_tfloat', ('int', 'Temporal'): 'tge_int_tint', ('Temporal', 'Temporal'): 'tge_temporal_temporal', ('text', 'Temporal'): 'tge_text_ttext', ('Temporal', 'float'): 'tge_tfloat_float', ('Temporal', 'int'): 'tge_tint_int', ('Temporal', 'text'): 'tge_ttext_text'} + + +def tge(*args): + """Portable ``#>=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tge', _T_tge, args) + + +_T_tgt = {('float', 'Temporal'): 'tgt_float_tfloat', ('int', 'Temporal'): 'tgt_int_tint', ('Temporal', 'Temporal'): 'tgt_temporal_temporal', ('text', 'Temporal'): 'tgt_text_ttext', ('Temporal', 'float'): 'tgt_tfloat_float', ('Temporal', 'int'): 'tgt_tint_int', ('Temporal', 'text'): 'tgt_ttext_text'} + + +def tgt(*args): + """Portable ``#>`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tgt', _T_tgt, args) + + +_T_tle = {('float', 'Temporal'): 'tle_float_tfloat', ('int', 'Temporal'): 'tle_int_tint', ('Temporal', 'Temporal'): 'tle_temporal_temporal', ('text', 'Temporal'): 'tle_text_ttext', ('Temporal', 'float'): 'tle_tfloat_float', ('Temporal', 'int'): 'tle_tint_int', ('Temporal', 'text'): 'tle_ttext_text'} + + +def tle(*args): + """Portable ``#<=`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tle', _T_tle, args) + + +_T_tlt = {('float', 'Temporal'): 'tlt_float_tfloat', ('int', 'Temporal'): 'tlt_int_tint', ('Temporal', 'Temporal'): 'tlt_temporal_temporal', ('text', 'Temporal'): 'tlt_text_ttext', ('Temporal', 'float'): 'tlt_tfloat_float', ('Temporal', 'int'): 'tlt_tint_int', ('Temporal', 'text'): 'tlt_ttext_text'} + + +def tlt(*args): + """Portable ``#<`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tlt', _T_tlt, args) + + +_T_tne = {('bool', 'Temporal'): 'tne_bool_tbool', ('Cbuffer', 'Temporal'): 'tne_cbuffer_tcbuffer', ('float', 'Temporal'): 'tne_float_tfloat', ('GSERIALIZED', 'Temporal'): 'tne_geo_trgeo', ('int', 'Temporal'): 'tne_int_tint', ('Jsonb', 'Temporal'): 'tne_jsonb_tjsonb', ('Pose', 'Temporal'): 'tne_pose_tpose', ('Temporal', 'bool'): 'tne_tbool_bool', ('Temporal', 'Cbuffer'): 'tne_tcbuffer_cbuffer', ('Temporal', 'Temporal'): 'tne_temporal_temporal', ('text', 'Temporal'): 'tne_text_ttext', ('Temporal', 'float'): 'tne_tfloat_float', ('Temporal', 'GSERIALIZED'): 'tne_trgeo_geo', ('Temporal', 'int'): 'tne_tint_int', ('Temporal', 'Jsonb'): 'tne_tjsonb_jsonb', ('Temporal', 'Npoint'): 'tne_tnpoint_npoint', ('Temporal', 'Pose'): 'tne_tpose_pose', ('Temporal', 'text'): 'tne_ttext_text'} + + +def tne(*args): + """Portable ``#<>`` (temporalComparison) -> pymeos_cffi backing call.""" + return _dispatch('tne', _T_tne, args) diff --git a/pymeos/temporal/temporal.py b/pymeos/temporal/temporal.py index b168ae66..8acd3504 100644 --- a/pymeos/temporal/temporal.py +++ b/pymeos/temporal/temporal.py @@ -592,15 +592,15 @@ def temporal_sample( temporal_tsample """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) if interpolation is None: intrp = self.interpolation() else: @@ -627,15 +627,15 @@ def temporal_precision( temporal_tprecision """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) elif isinstance(start, datetime): st = datetime_to_timestamptz(start) else: - st = pg_timestamptz_in(start, -1) + st = timestamptz_in(start, -1) if isinstance(duration, timedelta): dt = timedelta_to_interval(duration) else: - dt = pg_interval_in(duration, -1) + dt = interval_in(duration, -1) result = temporal_tprecision(self._inner, dt, st) return Temporal._factory(result) @@ -644,9 +644,9 @@ def to_instant(self) -> TI: Returns `self` as a :class:`TInstant`. MEOS Functions: - temporal_to_tinstant + temporal_as_tinstant """ - inst = temporal_to_tinstant(self._inner) + inst = temporal_as_tinstant(self._inner) return Temporal._factory(inst) def to_sequence(self, interpolation: TInterpolation) -> TS: @@ -656,7 +656,7 @@ def to_sequence(self, interpolation: TInterpolation) -> TS: MEOS Functions: temporal_to_sequence """ - seq = temporal_to_tsequence(self._inner, interpolation) + seq = temporal_as_tsequence(self._inner, interpolation) return Temporal._factory(seq) def to_sequenceset(self, interpolation: TInterpolation) -> TSS: @@ -664,9 +664,9 @@ def to_sequenceset(self, interpolation: TInterpolation) -> TSS: Returns `self` as a :class:`TSequenceSet`. MEOS Functions: - temporal_to_tsequenceset + temporal_as_tsequenceset """ - ss = temporal_to_tsequenceset(self._inner, interpolation) + ss = temporal_as_tsequenceset(self._inner, interpolation) return Temporal._factory(ss) def to_dataframe(self) -> pd.DataFrame: @@ -1160,7 +1160,7 @@ def frechet_distance(self, other: Temporal) -> float: MEOS Functions: temporal_frechet_distance """ - return temporal_frechet_distance(self._inner, other._inner) + return temporal_frechet_distance(self._inner, other._inner, spheroid=True) def dyntimewarp_distance(self, other: Temporal) -> float: """ @@ -1175,7 +1175,7 @@ def dyntimewarp_distance(self, other: Temporal) -> float: MEOS Functions: temporal_dyntimewarp_distance """ - return temporal_dyntimewarp_distance(self._inner, other._inner) + return temporal_dyntimewarp_distance(self._inner, other._inner, spheroid=True) def hausdorff_distance(self, other: Temporal) -> float: """ @@ -1190,7 +1190,7 @@ def hausdorff_distance(self, other: Temporal) -> float: MEOS Functions: temporal_hausdorff_distance """ - return temporal_hausdorff_distance(self._inner, other._inner) + return temporal_hausdorff_distance(self._inner, other._inner, spheroid=True) # ------------------------- Split Operations ------------------------------ def time_split( @@ -1217,11 +1217,11 @@ def time_split( temporal_time_split """ if start is None: - st = pg_timestamptz_in("2000-01-03", -1) + st = timestamptz_in("2000-01-03", -1) else: - st = datetime_to_timestamptz(start) if isinstance(start, datetime) else pg_timestamptz_in(start, -1) - dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else pg_interval_in(duration, -1) - fragments, times, count = temporal_time_split(self._inner, dt, st) + st = datetime_to_timestamptz(start) if isinstance(start, datetime) else timestamptz_in(start, -1) + dt = timedelta_to_interval(duration) if isinstance(duration, timedelta) else interval_in(duration, -1) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] @@ -1230,7 +1230,8 @@ def time_split_n(self, n: int) -> list[TG]: """ Returns a list of temporal objects of the same subtype as `self` with the same values as `self` but split in n temporal tiles of equal - duration. + duration. When `self` includes its end timestamp, that timestamp + starts an (n+1)th tile, which holds only the end value. Args: n: An :class:`int` with the number of temporal tiles. @@ -1245,7 +1246,7 @@ def time_split_n(self, n: int) -> list[TG]: return [self] st = temporal_start_timestamptz(self._inner) dt = timedelta_to_interval((self.end_timestamp() - self.start_timestamp()) / n) - fragments, times, count = temporal_time_split(self._inner, dt, st) + fragments, times, count = temporal_time_split(self._inner, dt, st, border_inc=True) from ..factory import _TemporalFactory return [_TemporalFactory.create_temporal(fragments[i]) for i in range(count)] diff --git a/pymeos/temporal/tinstant.py b/pymeos/temporal/tinstant.py index d79ffbce..5bd2f2d6 100644 --- a/pymeos/temporal/tinstant.py +++ b/pymeos/temporal/tinstant.py @@ -44,9 +44,7 @@ def __init__( self._inner = as_tinstant(self.__class__._parse_function(string)) else: ts = ( - datetime_to_timestamptz(timestamp) - if isinstance(timestamp, datetime) - else pg_timestamptz_in(timestamp, -1) + datetime_to_timestamptz(timestamp) if isinstance(timestamp, datetime) else timestamptz_in(timestamp, -1) ) self._inner = self.__class__._make_function(self.__class__._cast_function(value), ts) diff --git a/pyproject.toml b/pyproject.toml index 066b9e79..c7eb7fd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ license = { file = 'LICENSE' } requires-python = '>=3.10' dependencies = [ - 'pymeos-cffi >=1.3.0, <2', + 'pymeos-cffi >=1.4.0a1, <2', 'python-dateutil', 'shapely>=2.0.0', ] diff --git a/tests/aggregators/__init__.py b/tests/aggregators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/aggregators/aggregators_test.py b/tests/aggregators/aggregators_test.py new file mode 100644 index 00000000..51741a08 --- /dev/null +++ b/tests/aggregators/aggregators_test.py @@ -0,0 +1,178 @@ +import pytest + +from pymeos import ( + STBox, + TBoolSeq, + TBoolSeqSet, + TBox, + TFloatSeq, + TFloatSeqSet, + TGeomPointSeq, + TIntSeq, + TIntSeqSet, + TsTzSet, + TsTzSpan, + TsTzSpanSet, + TTextSeq, + TTextSeqSet, +) +from pymeos.aggregators import ( + TemporalAndAggregator, + TemporalAverageAggregator, + TemporalExtentAggregator, + TemporalFloatMaxAggregator, + TemporalFloatMinAggregator, + TemporalFloatSumAggregator, + TemporalInstantCountAggregator, + TemporalIntMaxAggregator, + TemporalIntMinAggregator, + TemporalIntSumAggregator, + TemporalNumberExtentAggregator, + TemporalOrAggregator, + TemporalPeriodCountAggregator, + TemporalPointExtentAggregator, + TemporalTextMaxAggregator, + TemporalTextMinAggregator, + TimeContinuousUnionAggregator, + TimeInstantaneousUnionAggregator, +) +from tests.conftest import TestPyMEOS + + +class TestAggregators(TestPyMEOS): + """Every aggregator on two overlapping inputs, laid out as #TestTBoxConstructors is.""" + + ints = [TIntSeq("[1@2000-01-01, 3@2000-01-03]"), TIntSeq("[2@2000-01-02, 2@2000-01-04]")] + floats = [TFloatSeq("[1@2000-01-01, 3@2000-01-03]"), TFloatSeq("[2@2000-01-02, 2@2000-01-04]")] + bools = [TBoolSeq("[t@2000-01-01, f@2000-01-03]"), TBoolSeq("[t@2000-01-02, t@2000-01-04]")] + texts = [TTextSeq('["a"@2000-01-01, "c"@2000-01-03]'), TTextSeq('["b"@2000-01-02, "b"@2000-01-04]')] + points = [ + TGeomPointSeq("[POINT(0 0)@2000-01-01, POINT(2 2)@2000-01-03]"), + TGeomPointSeq("[POINT(1 3)@2000-01-02, POINT(1 3)@2000-01-04]"), + ] + spans = [TsTzSpan("[2000-01-01, 2000-01-03]"), TsTzSpan("[2000-01-02, 2000-01-04]")] + sets = [TsTzSet("{2000-01-01, 2000-01-03}"), TsTzSet("{2000-01-02, 2000-01-03}")] + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalAndAggregator, + bools, + TBoolSeqSet("{[t@2000-01-01, f@2000-01-03], (t@2000-01-03, t@2000-01-04]}"), + ), + (TemporalOrAggregator, bools, TBoolSeqSet("{[t@2000-01-01, t@2000-01-04]}")), + ( + TemporalIntMaxAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntMinAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalIntSumAggregator, + ints, + TIntSeqSet("{[1@2000-01-01, 3@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMaxAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 3@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalFloatMinAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-04]}"), + ), + ( + TemporalFloatSumAggregator, + floats, + TFloatSeqSet( + "{[1@2000-01-01, 2@2000-01-02), [4@2000-01-02, 5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}" + ), + ), + ( + TemporalAverageAggregator, + floats, + TFloatSeqSet("{[1@2000-01-01, 2@2000-01-02, 2.5@2000-01-03], (2@2000-01-03, 2@2000-01-04]}"), + ), + ( + TemporalTextMaxAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-02, "c"@2000-01-03], ("b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalTextMinAggregator, + texts, + TTextSeqSet('{["a"@2000-01-01, "b"@2000-01-03, "b"@2000-01-04]}'), + ), + ( + TemporalInstantCountAggregator, + sets, + TIntSeq("{1@2000-01-01, 1@2000-01-02, 2@2000-01-03}"), + ), + ( + TemporalPeriodCountAggregator, + spans, + TIntSeqSet("{[1@2000-01-01, 2@2000-01-02, 2@2000-01-03], (1@2000-01-03, 1@2000-01-04]}"), + ), + ( + TimeInstantaneousUnionAggregator, + sets, + TsTzSet("{2000-01-01, 2000-01-02, 2000-01-03}"), + ), + ( + TimeContinuousUnionAggregator, + spans, + TsTzSpanSet("{[2000-01-01, 2000-01-04]}"), + ), + ], + ids=[ + "And", + "Or", + "IntMax", + "IntMin", + "IntSum", + "FloatMax", + "FloatMin", + "FloatSum", + "Average", + "TextMax", + "TextMin", + "InstantCount", + "PeriodCount", + "InstantaneousUnion", + "ContinuousUnion", + ], + ) + def test_aggregate(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + @pytest.mark.parametrize( + "aggregator, inputs, expected", + [ + ( + TemporalNumberExtentAggregator, + floats, + TBox("TBOXFLOAT XT([1, 3],[2000-01-01, 2000-01-04])"), + ), + ( + TemporalPointExtentAggregator, + points, + STBox("STBOX XT(((0,0),(2,3)),[2000-01-01, 2000-01-04])"), + ), + (TemporalExtentAggregator, ints, TsTzSpan("[2000-01-01, 2000-01-04]")), + ], + ids=["NumberExtent", "PointExtent", "TimeExtent"], + ) + def test_extent(self, aggregator, inputs, expected): + assert aggregator.aggregate(inputs) == expected + + def test_incremental_aggregation(self): + aggregation = TemporalIntSumAggregator.start_aggregation() + for temporal in self.ints: + aggregation.add(temporal) + assert aggregation.aggregation() == TemporalIntSumAggregator.aggregate(self.ints) diff --git a/tests/boxes/stbox_test.py b/tests/boxes/stbox_test.py index bcc676d9..77588045 100644 --- a/tests/boxes/stbox_test.py +++ b/tests/boxes/stbox_test.py @@ -1261,3 +1261,24 @@ def test_gt(self): def test_ge(self): _ = self.stbxt >= self.other + + +class TestSTBoxTiling(TestSTBox): + # Mirrors #test_time_split of the temporal split tests, on the space tiles + trip = TGeomPointSeq( + "SRID=3857;[Point(481000 6586000)@2020-06-01 08:00:00+00, Point(487000 6588000)@2020-06-01 08:30:00+00]" + ) + + def test_tile(self): + tiles = STBox.from_tpoint(self.trip).tile(5e3) + assert [str(tile) for tile in tiles] == [ + "SRID=3857;STBOX X((480000,6585000),(485000,6590000))", + "SRID=3857;STBOX X((485000,6585000),(490000,6590000))", + ] + + def test_tpoint_tile(self): + pieces = self.trip.tile(5e3, remove_empty=True) + assert [str(piece) for piece in pieces] == [ + "{[POINT(481000 6586000)@2020-06-01 08:00:00+00, POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00]}", + "{[POINT(485000 6587333.333333333)@2020-06-01 08:20:00+00, POINT(487000 6588000)@2020-06-01 08:30:00+00]}", + ] diff --git a/tests/boxes/tbox_test.py b/tests/boxes/tbox_test.py index c25a86e2..1ec4edc9 100644 --- a/tests/boxes/tbox_test.py +++ b/tests/boxes/tbox_test.py @@ -1123,3 +1123,15 @@ def test_gt(self): def test_ge(self): _ = self.tbfxt >= self.other + + +class TestTBoxTiling(TestTBox): + # Mirrors #test_value_time_split of the temporal split tests, on the value-time tiles + def test_tile(self): + box = TBox("TBOXFLOAT XT([0, 33.35670867892772],[2020-06-01 08:00:15.533+02, 2020-06-06 01:53:54.08198+02])") + tiles = box.tile(10, "1 day", 0.0, "2020-06-01 00:00:00+02") + assert tiles == [ + TBox(f"TBOXFLOAT XT([{v}, {v + 10}),[2020-06-0{d} 00:00:00+02, 2020-06-0{d + 1} 00:00:00+02))") + for d in range(1, 7) + for v in (0, 10, 20, 30) + ] diff --git a/tests/main/tbool_test.py b/tests/main/tbool_test.py index 92400c62..46e95ac4 100644 --- a/tests/main/tbool_test.py +++ b/tests/main/tbool_test.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone import pytest +from pymeos_cffi import MeosInvalidArgTypeError from pymeos import ( TBool, @@ -345,8 +346,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -354,7 +355,7 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -364,8 +365,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -374,8 +375,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -387,8 +388,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -397,8 +398,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -410,8 +411,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingBoolean",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -422,8 +423,8 @@ def test_as_hexwkb(self, temporal, expected): " false\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -434,8 +435,8 @@ def test_as_hexwkb(self, temporal, expected): " true\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -822,7 +823,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tbi, 440045287), (tbds, 2385901957), (tbs, 2385901957), (tbss, 1543175996)], + [(tbi, 440045318), (tbds, 3614979951), (tbs, 3614979951), (tbss, 1190925880)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -1490,35 +1491,6 @@ def test_when_false(self, temporal, expected): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, TBoolInst("True@2019-09-01")), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -1589,35 +1561,6 @@ def test_at_min(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{False@2019-09-02}")), - (tbs, TBoolSeq("[False@2019-09-02]")), - (tbss, TBoolSeqSet("{[False@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - assert temporal.minus_max() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tbi, None), - (tbds, TBoolSeq("{True@2019-09-01}")), - (tbs, TBoolSeq("[True@2019-09-01, True@2019-09-02)")), - ( - tbss, - TBoolSeqSet("{[True@2019-09-01, True@2019-09-02),[True@2019-09-03, True@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - assert temporal.minus_min() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -1681,9 +1624,10 @@ def test_at_minus(self, temporal, restrictor): [tbi, tbds, tbs, tbss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TBool.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TBool.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTBoolComparisons(TestTBool): diff --git a/tests/main/tfloat_test.py b/tests/main/tfloat_test.py index b3e97c59..41c1089a 100644 --- a/tests/main/tfloat_test.py +++ b/tests/main/tfloat_test.py @@ -401,8 +401,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -410,7 +410,7 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -424,8 +424,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -434,8 +434,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -451,8 +451,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -461,8 +461,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -478,8 +478,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -490,8 +490,8 @@ def test_as_hexwkb(self, temporal, expected): " 2.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -502,8 +502,8 @@ def test_as_hexwkb(self, temporal, expected): " 1.5\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -958,12 +958,12 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tfi, 1307112078), - (tfds, 1935376725), - (tfs, 1935376725), - (tfss, 4247071962), - (tfs, 1935376725), - (tfss, 4247071962), + (tfi, 1307112109), + (tfds, 3164454719), + (tfs, 3164454719), + (tfss, 1518183798), + (tfs, 3164454719), + (tfss, 1518183798), ], ids=[ "Instant", @@ -2836,8 +2836,9 @@ def test_value_split(self, temporal, expected): ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2854,15 +2855,17 @@ def test_time_split(self, temporal, expected): ( tfs, [ - TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00:00+00)"), - TFloatSeq("[1.5@2019-09-01 12:00:00+00, 2@2019-09-02]"), + TFloatSeq("[1@2019-09-01, 1.5@2019-09-01 12:00)"), + TFloatSeq("[1.5@2019-09-01 12:00, 2@2019-09-02)"), + TFloatSeq("[2@2019-09-02]"), ], ), ( tfss, [ - TFloatSeq("[1@2019-09-01,2@2019-09-02]"), - TFloatSeq("[1@2019-09-03, 1@2019-09-05]"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2886,9 +2889,10 @@ def test_time_split_n(self, temporal, expected): ( tfss, [ - TFloatSeq("{[1@2019-09-01, 2@2019-09-02)}"), - TFloatSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TFloatSeq("{[2@2019-09-02]}"), + TFloatSeqSet("{[1@2019-09-01, 2@2019-09-02)}"), + TFloatSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TFloatSeqSet("{[1@2019-09-05]}"), + TFloatSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/tgeogpoint_test.py b/tests/main/tgeogpoint_test.py index 4e415c37..4f3abeb2 100644 --- a/tests/main/tgeogpoint_test.py +++ b/tests/main/tgeogpoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest import shapely.geometry -from pymeos_cffi import MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import LineString, Point, set_srid from pymeos import ( @@ -436,6 +436,12 @@ def test_as_hexwkb(self, temporal, expected): tpi, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -447,8 +453,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -459,7 +465,7 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -468,6 +474,12 @@ def test_as_hexwkb(self, temporal, expected): tpds, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -479,8 +491,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -495,8 +507,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -507,6 +519,12 @@ def test_as_hexwkb(self, temporal, expected): tps, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -518,8 +536,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -534,8 +552,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -546,6 +564,12 @@ def test_as_hexwkb(self, temporal, expected): tpss, "{\n" ' "type": "MovingPoint",\n' + ' "crs": {\n' + ' "type": "Name",\n' + ' "properties": {\n' + ' "name": "EPSG:4326"\n' + " }\n" + " },\n" ' "bbox": [\n' " [\n" " 1,\n" @@ -557,8 +581,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -575,8 +599,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -593,8 +617,8 @@ def test_as_hexwkb(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -984,7 +1008,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tpi, 1181779687), (tpds, 1545137628), (tps, 1545137628), (tpss, 1008965061)], + [(tpi, 1181779718), (tpds, 2774215622), (tps, 2774215622), (tpss, 429936847)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2235,9 +2259,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeogPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeogPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeogPointEverSpatialOperations(TestTGeogPoint): @@ -2470,8 +2495,9 @@ class TestTGeogPointSplitOperations(TestTGeogPoint): ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -2494,15 +2520,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeogPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeogPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeogPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeogPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeogPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeogPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeogPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeogPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tgeompoint_test.py b/tests/main/tgeompoint_test.py index d08c6a03..fa647db8 100644 --- a/tests/main/tgeompoint_test.py +++ b/tests/main/tgeompoint_test.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from pymeos_cffi import MeosInvalidArgValueError +from pymeos_cffi import MeosInvalidArgTypeError, MeosInvalidArgValueError from shapely import GeometryCollection, LineString, MultiPoint, Point, Polygon from pymeos import ( @@ -466,8 +466,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -478,7 +478,7 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -498,8 +498,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -514,8 +514,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -537,8 +537,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -553,8 +553,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -576,8 +576,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -594,8 +594,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -612,8 +612,8 @@ def test_as_geojson(self, temporal, expected): " ]\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -1010,10 +1010,10 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", [ - (tpi, 382694564), - (tpds, 1664033448), - (tps, 1664033448), - (tpss, 2878566103), + (tpi, 382694595), + (tpds, 2893111442), + (tps, 2893111442), + (tpss, 1203500793), ], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) @@ -2130,35 +2130,6 @@ class TestTGeomPointRestrictors(TestTGeomPoint): def test_at(self, temporal, restrictor, expected): assert temporal.at(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - ( - tpss, - TGeomPointSeqSet("{[Point(1 1)@2019-09-01],[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}"), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_min(self, temporal, expected): - assert temporal.at_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_at_max(self, temporal, expected): - assert temporal.at_max() == expected - @pytest.mark.parametrize( "temporal, restrictor, expected", [ @@ -2259,43 +2230,6 @@ def test_at_max(self, temporal, expected): def test_minus(self, temporal, restrictor, expected): assert temporal.minus(restrictor) == expected - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(2 2)@2019-09-02]}")), - (tps, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - (tpss, TGeomPointSeqSet("{(Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]}")), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_min(self, temporal, expected): - if expected is None: - assert temporal.minus_min() is None - else: - assert temporal.minus_min() == expected - - @pytest.mark.parametrize( - "temporal, expected", - [ - (tpi, None), - (tpds, TGeomPointSeqSet("{[Point(1 1)@2019-09-01]}")), - (tps, TGeomPointSeqSet("{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02)}")), - ( - tpss, - TGeomPointSeqSet( - "{[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02),[Point(1 1)@2019-09-03,Point(1 1)@2019-09-05]}" - ), - ), - ], - ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], - ) - def test_minus_max(self, temporal, expected): - if expected is None: - assert temporal.minus_max() is None - else: - assert temporal.minus_max() == expected - @pytest.mark.parametrize( "temporal, restrictor", [ @@ -2359,9 +2293,10 @@ def test_at_minus(self, temporal, restrictor): [tpi, tpds, tps, tpss], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) - def test_at_minus_min_max(self, temporal): - assert TGeomPoint.from_merge(temporal.at_min(), temporal.minus_min()) == temporal - assert TGeomPoint.from_merge(temporal.at_max(), temporal.minus_max()) == temporal + def test_min_max_restrictors_raise(self, temporal): + for restrict in (temporal.at_min, temporal.at_max, temporal.minus_min, temporal.minus_max): + with pytest.raises(MeosInvalidArgTypeError): + restrict() class TestTGeomPointTopologicalFunctions(TestTGeomPoint): @@ -3140,8 +3075,9 @@ class TestTGeomPointSplitOperations(TestTGeomPoint): ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], @@ -3164,15 +3100,17 @@ def test_time_split(self, temporal, expected): ( tps, [ - TGeomPointSeq("[Point(1 1)@2019-09-01, Point(1.5 1.5)@2019-09-01 12:00:00+00)"), - TGeomPointSeq("[Point(1.5 1.5)@2019-09-01 12:00:00+00, Point(2 2)@2019-09-02]"), + TGeomPointSeq("[POINT(1 1)@2019-09-01, POINT(1.5 1.5)@2019-09-01 12:00)"), + TGeomPointSeq("[POINT(1.5 1.5)@2019-09-01 12:00, POINT(2 2)@2019-09-02)"), + TGeomPointSeq("[POINT(2 2)@2019-09-02]"), ], ), ( tpss, [ - TGeomPointSeq("[Point(1 1)@2019-09-01,Point(2 2)@2019-09-02]"), - TGeomPointSeq("[Point(1 1)@2019-09-03, Point(1 1)@2019-09-05]"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-01, POINT(2 2)@2019-09-02]}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-03, POINT(1 1)@2019-09-05)}"), + TGeomPointSeqSet("{[POINT(1 1)@2019-09-05]}"), ], ), ], diff --git a/tests/main/tint_test.py b/tests/main/tint_test.py index 42431b0c..6380fce6 100644 --- a/tests/main/tint_test.py +++ b/tests/main/tint_test.py @@ -355,8 +355,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -364,7 +364,7 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -378,8 +378,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -388,8 +388,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -405,8 +405,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -415,8 +415,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -432,8 +432,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -444,8 +444,8 @@ def test_as_hexwkb(self, temporal, expected): " 2\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -456,8 +456,8 @@ def test_as_hexwkb(self, temporal, expected): " 1\n" " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -882,7 +882,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tii, 440045287), (tids, 3589664982), (tis, 3589664982), (tiss, 205124107)], + [(tii, 440045318), (tids, 523775680), (tis, 523775680), (tiss, 2660990281)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): @@ -2196,7 +2196,7 @@ class TestTIntTopologicalFunctions(TestTInt): "temporal, argument, expected", [ (tii, TIntInst("1@2019-09-02"), False), - (tii, TIntInst("2@2019-09-02"), True), + (tii, TIntInst("2@2019-09-01"), True), (tids, TIntInst("1@2019-09-03"), False), (tids, TIntInst("3@2019-09-02"), True), (tis, TIntInst("1@2019-09-03"), False), @@ -2573,8 +2573,9 @@ def test_value_split(self, temporal, expected): ( tiss, [ - TIntSeq("[1@2019-09-01,2@2019-09-02]"), - TIntSeq("[1@2019-09-03, 1@2019-09-05]"), + TIntSeqSet("{[1@2019-09-01, 2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), ], ), ], @@ -2592,9 +2593,10 @@ def test_time_split(self, temporal, expected): ( tiss, [ - TIntSeq("{[1@2019-09-01, 1@2019-09-02)}"), - TIntSeq("{[1@2019-09-03, 1@2019-09-05]}"), - TIntSeq("{[2@2019-09-02]}"), + TIntSeqSet("{[1@2019-09-01, 1@2019-09-02)}"), + TIntSeqSet("{[1@2019-09-03, 1@2019-09-05)}"), + TIntSeqSet("{[1@2019-09-05]}"), + TIntSeqSet("{[2@2019-09-02]}"), ], ), ], diff --git a/tests/main/ttext_test.py b/tests/main/ttext_test.py index c575a447..0d109bf0 100644 --- a/tests/main/ttext_test.py +++ b/tests/main/ttext_test.py @@ -358,8 +358,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-01T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-01T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -367,7 +367,7 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00"\n' " ],\n" ' "interpolation": "None"\n' "}", @@ -377,8 +377,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -387,8 +387,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -400,8 +400,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-02T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-02T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -410,8 +410,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true,\n' @@ -423,8 +423,8 @@ def test_as_hexwkb(self, temporal, expected): "{\n" ' "type": "MovingText",\n' ' "period": {\n' - ' "begin": "2019-09-01T00:00:00+00",\n' - ' "end": "2019-09-05T00:00:00+00",\n' + ' "begin": "2019-09-01T00:00:00+00:00",\n' + ' "end": "2019-09-05T00:00:00+00:00",\n' ' "lower_inc": true,\n' ' "upper_inc": true\n' " },\n" @@ -435,8 +435,8 @@ def test_as_hexwkb(self, temporal, expected): ' "BBB"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-01T00:00:00+00",\n' - ' "2019-09-02T00:00:00+00"\n' + ' "2019-09-01T00:00:00+00:00",\n' + ' "2019-09-02T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -447,8 +447,8 @@ def test_as_hexwkb(self, temporal, expected): ' "AAA"\n' " ],\n" ' "datetimes": [\n' - ' "2019-09-03T00:00:00+00",\n' - ' "2019-09-05T00:00:00+00"\n' + ' "2019-09-03T00:00:00+00:00",\n' + ' "2019-09-05T00:00:00+00:00"\n' " ],\n" ' "lower_inc": true,\n' ' "upper_inc": true\n' @@ -838,7 +838,7 @@ def test_segments(self, temporal, expected): @pytest.mark.parametrize( "temporal, expected", - [(tti, 1893808825), (ttds, 1223816819), (tts, 1223816819), (ttss, 2199213310)], + [(tti, 1893808856), (ttds, 2452894813), (tts, 2452894813), (ttss, 3439543446)], ids=["Instant", "Discrete Sequence", "Sequence", "SequenceSet"], ) def test_hash(self, temporal, expected): diff --git a/tests/test_portable_parity.py b/tests/test_portable_parity.py new file mode 100644 index 00000000..2915c4e3 --- /dev/null +++ b/tests/test_portable_parity.py @@ -0,0 +1,160 @@ +"""Portable bare-name parity gate for PyMEOS. + +The canonical portable dialect (RFC #920; contract: +MobilityDB/MEOS-API ``meta/portable-aliases.json``, vendored verbatim under +``tools/portable_aliases/``) is the single cross-binding source of truth. +This test is the PyMEOS analogue of MobilityDB's +``tools/portable_aliases/generate.py --check`` and MEOS-API's +``portable_parity.py``: it asserts that the generated ``pymeos.portable`` +API exposes EVERY canonical bare name, each backed by a real pymeos_cffi +function, across all six in-scope type families -- 0 unbacked, zero +per-binding exceptions. It runs without the compiled binding (operating on +the contract + the committed generated module + the MEOS function universe), +and additionally verifies against the live pymeos_cffi when present. + + python3 -m pytest tests/test_portable_parity.py + python3 tests/test_portable_parity.py +""" + +import ast +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TOOLS = ROOT / "tools" / "portable_aliases" +sys.path.insert(0, str(TOOLS)) + +from portable import attach_portable_aliases # vendored from SoT +from portable_parity import build_parity # vendored from SoT +import generate as gen + +CONTRACT = TOOLS / "portable-aliases.json" +GENERATED = ROOT / "pymeos" / "portable.py" + + +def _contract(): + return attach_portable_aliases({"functions": []}, CONTRACT)["portableAliases"] + + +def _resolve_universe(): + """(universe, source). Live pymeos_cffi if built (CI), else the in-tree + MEOS headers, else (None, None) -> universe-dependent checks skip.""" + try: + return gen.universe_from_cffi(), "pymeos_cffi" + except Exception: + pass + dirs = gen._default_header_dirs() + if dirs: + return gen.universe_from_headers(dirs), f"headers:{dirs}" + return None, None + + +def _committed_all_and_tables(): + """AST-read the committed module without importing it (no binding needed).""" + tree = ast.parse(GENERATED.read_text()) + all_names, tables = None, {} + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + name = getattr(node.targets[0], "id", "") + if name == "__all__": + all_names = ast.literal_eval(node.value) + elif name.startswith("_T_"): + tables[name[3:]] = ast.literal_eval(node.value) + return all_names, tables + + +class ContractIntegrity(unittest.TestCase): + def test_29_bijective_pairs(self): + pa = _contract() + self.assertEqual(pa["count"], 29) + self.assertEqual(len(pa["bareNames"]), 29) + self.assertEqual(len(pa["byOperator"]), 29) + self.assertEqual(len(pa["byBareName"]), 29) + self.assertEqual( + pa["scope"]["inScopeTypeFamilies"], + ["temporal", "geo", "cbuffer", "npoint", "pose", "rgeo"]) + + +class CommittedModule(unittest.TestCase): + """The shipped pymeos/portable.py must expose every canonical bare name + with a real, non-empty backing table -- zero per-binding exceptions.""" + + def test_exposes_every_bare_name(self): + pa = _contract() + all_names, tables = _committed_all_and_tables() + self.assertEqual(sorted(all_names), sorted(pa["bareNames"])) + self.assertEqual(set(tables), set(pa["bareNames"])) + empty = sorted(b for b, t in tables.items() if not t) + self.assertEqual(empty, [], f"bare names with no backing: {empty}") + + def test_every_entry_routes_to_a_real_symbol(self): + _, tables = _committed_all_and_tables() + for bare, tbl in tables.items(): + for key, sym in tbl.items(): + self.assertIsInstance(sym, str) + self.assertRegex(sym, r"^[a-z][a-z0-9_]+$", + f"{bare}: bad backing symbol {sym!r}") + + +@unittest.skipIf(_resolve_universe()[0] is None, + "no pymeos_cffi and no MEOS headers available") +class ParityGate(unittest.TestCase): + """Byte-identical to the MEOS-API SoT parity logic: 29/29, 0 unbacked.""" + + def setUp(self): + self.universe, self.source = _resolve_universe() + + def test_29_of_29_backed_zero_unbacked(self): + rep = build_parity(attach_portable_aliases( + {"functions": [{"name": n} for n in self.universe]}, CONTRACT)) + self.assertEqual(rep["total"], 29) + self.assertEqual(rep["needsExplicitBacking"], 0, + f"unbacked: {rep['unbacked']}") + self.assertEqual(rep["backed"], 29) + self.assertEqual(rep["parityPct"], 100.0) + + def test_all_six_families_present(self): + pa = _contract() + in_scope = pa["scope"]["inScopeTypeFamilies"] + fam_re = gen.family_regexes(in_scope) + witnessed = set() + for bare in pa["bareNames"]: + syms, _ = gen.backing_symbols( + bare, pa.get("explicitBacking", {}).get(bare, []), + self.universe) + for s in syms: + for fam, rx in fam_re.items(): + if rx.search(s): + witnessed.add(fam) + missing = [f for f in in_scope if f not in witnessed] + self.assertEqual(missing, [], f"families with no backing: {missing}") + + +@unittest.skipIf(_resolve_universe()[0] is None, + "no pymeos_cffi and no MEOS headers available") +class CommittedModuleValidity(unittest.TestCase): + """Every backing symbol the shipped module dispatches to must exist in + the resolved universe -- i.e. the committed ``pymeos/portable.py`` is + valid for the binding actually under test (catches hand edits and stale + drift that would break runtime, while tolerating benign MEOS overload + additions across versions, unlike a brittle byte-for-byte snapshot). + """ + + def test_every_committed_symbol_is_real(self): + universe, _ = _resolve_universe() + _, tables = _committed_all_and_tables() + unknown = sorted({ + sym for tbl in tables.values() for sym in tbl.values() + if sym not in universe + }) + self.assertEqual( + unknown, [], + f"pymeos/portable.py dispatches to {len(unknown)} symbol(s) " + f"absent from the binding -- regenerate with " + f"tools/portable_aliases/generate.py: {unknown[:10]}") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tools/portable_aliases/README.md b/tools/portable_aliases/README.md new file mode 100644 index 00000000..583966e5 --- /dev/null +++ b/tools/portable_aliases/README.md @@ -0,0 +1,48 @@ +# Portable bare-name dialect (PyMEOS) + +PyMEOS exposes every MobilityDB operator under a single **portable bare +name** (`overlaps`, `contains`, `teq`, `nearestApproachDistance`, …) so one +query reads the same across every binding/engine. + +## Source of truth + +`portable-aliases.json` is the canonical contract, vendored **verbatim** +from `MobilityDB/MEOS-API` (`meta/portable-aliases.json`, RFC #920; +discussion MobilityDB #861; native in MobilityDB via the 1303-alias +PR #1075). `portable.py` and `portable_parity.py` are likewise vendored +verbatim from MEOS-API so PyMEOS's parity logic is byte-identical to the +ecosystem source of truth. Update them only by re-vendoring from MEOS-API. + +The mapping is **29 operator → bare-name pairs**, type-agnostic across all +six in-scope temporal type families: `temporal, geo, cbuffer, npoint, pose, +rgeo`. + +## What is generated + +`pymeos/portable.py` (GENERATED — do not edit) defines one callable per +bare name. Each dispatches, by the runtime C type of its arguments, to the +**exact `pymeos_cffi` function** the operator is backed by — the same native +call the type-qualified API uses. Nothing is reimplemented: every bare name +is identical to its operator by construction. + +## Regenerate + +``` +python3 tools/portable_aliases/generate.py # uses MEOS at the + # install prefix +python3 tools/portable_aliases/generate.py --check # parity gate +python3 tools/portable_aliases/generate.py --cffi --check # also assert the + # live built binding +``` + +`MEOS_INCLUDE=` (or `--headers `) selects the MEOS headers; the +default is the install prefix, then a sibling MobilityDB checkout. + +## Gate + +`tests/test_portable_parity.py` (run by `pytest`, and the explicit CI step) +asserts: the contract is 29 bijective pairs; the generated API exposes every +bare name with a non-empty backing table; parity is **29/29 backed, 0 +unbacked** (byte-identical to the MEOS-API logic); all six in-scope families +are witnessed; and every committed backing symbol exists in the binding +under test. Zero per-binding exceptions. diff --git a/tools/portable_aliases/generate.py b/tools/portable_aliases/generate.py new file mode 100644 index 00000000..18608956 --- /dev/null +++ b/tools/portable_aliases/generate.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# Generate the portable bare-name dialect for PyMEOS. +# +# The canonical portable bare-name mapping (RFC #920, MobilityDB #861) is the +# single cross-binding codegen source of truth: every binding/engine exposes +# the SAME bare names (overlaps, contains, teq, ...) so a user learns one +# reference and assumes the rest. The mapping is type-agnostic -- it applies +# to every temporal type family. +# +# This tool reads +# * the vendored contract tools/portable_aliases/portable-aliases.json +# (verbatim from MobilityDB/MEOS-API; folded into the catalog shape with +# the vendored attach_portable_aliases, byte-identical to the SoT), and +# * the MEOS function universe -- the exact C functions PyMEOS exposes +# through pymeos_cffi -- obtained from the MEOS headers (default) or, in +# CI where pymeos_cffi is built, by introspecting it directly, +# and emits pymeos/portable.py : one callable per canonical bare name that +# DISPATCHES, by the runtime C type of its arguments, to the EXACT pymeos_cffi +# backing function the corresponding operator uses. No behaviour is +# reimplemented -- every leaf is a direct pymeos_cffi call, so each bare name +# is identical to the operator by construction. +# +# Usage: +# python3 tools/portable_aliases/generate.py --headers \ +# [--out pymeos/portable.py] [--report build/portable_parity.json] +# python3 tools/portable_aliases/generate.py --cffi --check # CI gate + +import argparse +import json +import os +import re +import sys +from collections import defaultdict +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +from portable import attach_portable_aliases # vendored from MEOS-API (SoT) +from portable_parity import build_parity # vendored from MEOS-API (SoT) + +CONTRACT = HERE / "portable-aliases.json" + +# Param/return C base type -> the runtime token the generated dispatcher keys +# on. MEOS passes every temporal subtype as `const Temporal *`, so all +# temporal overloads of an operator collapse onto one generic symbol keyed by +# `Temporal` -- which is why this stays small and unambiguous. +SCALAR_PY_TOKEN = { + "bool": "bool", "int": "int", "int32": "int", "int64": "bigint", + "long": "bigint", "double": "float", "float8": "float", + "text": "text", "TimestampTz": "timestamptz", "Timestamp": "timestamptz", + "DateADT": "date", "Interval": "interval", +} + +# Header declaration: extern (); (one per line in MEOS) +_DECL = re.compile( + r"^\s*extern\s+(.+?[\s*])\b([A-Za-z_]\w*)\s*\(([^;{]*)\)\s*;", re.M) + + +def _base_ctype(decl: str) -> str: + """Last type identifier of a C declarator, sans const / * / arg name.""" + toks = re.findall(r"[A-Za-z_]\w*", decl.replace("const", " ")) + if not toks: + return "" + # `const STBox *box1` -> ['STBox','box1']; `int i` -> ['int','i']; + # `double` -> ['double']. The base type is the token before the name, + # or the only token. A pointer means the prior token is the type. + if "*" in decl: + return toks[0] + return toks[0] if len(toks) == 1 else toks[-2] if len(toks) > 1 else toks[0] + + +def _params(raw: str): + raw = raw.strip() + if not raw or raw == "void": + return () + out = [] + for p in raw.split(","): + p = p.strip() + if p in ("...", ""): + out.append("...") + continue + out.append(_base_ctype(p)) + return tuple(out) + + +def universe_from_headers(dirs): + """{name: (ret_base, (param_base, ...))} parsed from MEOS *.h files.""" + funcs = {} + files = [] + for d in dirs: + d = Path(d) + if d.is_file(): + files.append(d) + elif d.is_dir(): + files += sorted(d.glob("meos*.h")) + if not files: + sys.exit(f"no MEOS headers found in {dirs} -- pass --headers ") + for fp in files: + text = fp.read_text(encoding="utf-8", errors="ignore") + text = re.sub(r"/\*.*?\*/", " ", text, flags=re.S) + text = re.sub(r"//.*", " ", text) + for m in _DECL.finditer(text): + ret, name, params = m.group(1), m.group(2), m.group(3) + funcs[name] = (_base_ctype(ret + " r"), _params(params)) + return funcs + + +def universe_from_cffi(): + """Names actually exported by the installed pymeos_cffi (CI path).""" + import pymeos_cffi # noqa: only available where the binding is built + return { + n: (None, None) + for n in dir(pymeos_cffi) + if not n.startswith("_") and callable(getattr(pymeos_cffi, n)) + } + + +def backing_symbols(bare, explicit_prefixes, universe): + """Mirror portable_parity: name == bare or startswith bare+'_'; else the + verified explicit-backing C prefixes (e.g. nearestApproachDistance->nad).""" + def match(pfx): + return [n for n in universe if n == pfx or n.startswith(pfx + "_")] + + hits, via = match(bare), "prefix" + if not hits: + for pfx in explicit_prefixes: + hits += match(pfx) + via = "explicit" if hits else None + return sorted(set(hits)), via + + +def token(ctype): + """C base type -> dispatcher token (Temporal stays Temporal; scalars + fold to a stable Python-facing token).""" + return SCALAR_PY_TOKEN.get(ctype, ctype) + + +# Each in-scope type family is *witnessed* when some backing symbol of some +# canonical bare name carries that family's MEOS name token -- the same +# per-family convention MobilityDB's tools/portable_aliases/generate.py and +# its six mobilitydb/sql//..._portable_aliases.in.sql files use. This +# proves cbuffer/npoint/pose/rgeo are covered exactly like every other type +# (the hard invariant: never a headline exclusion), without inventing a gate +# that diverges from the SoT's type-agnostic 29/29 parity. +FAMILY_PATTERNS = { + "temporal": r"(^|_)(temporal|tspatial|tnumber|tbool|tint|tfloat|ttext)(_|$)", + "geo": r"(^|_)(geo|tgeo|geom|geog|tgeompoint|tgeogpoint|tpoint)(_|$)", + "cbuffer": r"cbuffer", + "npoint": r"npoint", + "pose": r"(^|_)t?pose(_|$)", + "rgeo": r"(t?rgeo|trgeometry|rgeometry)", +} + + +def family_regexes(in_scope): + return {f: re.compile(FAMILY_PATTERNS.get(f, re.escape(f)), re.I) + for f in in_scope} + + +HEADER = '''\ +# Copyright (c) 2016-2026, Université libre de Bruxelles and PyMEOS +# contributors. Licensed under the PostgreSQL License (see LICENSE). +# +# ============================================================================ +# GENERATED by tools/portable_aliases/generate.py -- DO NOT EDIT. +# Regenerate: python3 tools/portable_aliases/generate.py --headers +# ============================================================================ +# +# The portable bare-name dialect (RFC #920; canonical contract: +# MobilityDB/MEOS-API meta/portable-aliases.json; native in MobilityDB via +# the 1303-alias PR #1075). Every binding exposes the IDENTICAL bare names +# from that one mapping, type-agnostically across all six temporal type +# families (temporal, geo, cbuffer, npoint, pose, rgeo). +# +# Each bare name dispatches, by the runtime C type of its arguments, to the +# EXACT pymeos_cffi function the operator is backed by -- the same native +# call the type-qualified API uses. Nothing is reimplemented: every bare +# name is identical to its operator by construction. +"""Portable bare-name dialect for PyMEOS (generated -- see module comment).""" +from __future__ import annotations + +import datetime as _dt + +# Resolved at call time so importing this module never requires the compiled +# binding to be present (e.g. docs / parity checks). +_cffi = None + + +def _pymeos_cffi(): + global _cffi + if _cffi is None: + import pymeos_cffi as _c + + _cffi = _c + return _cffi + + +def _unwrap(a): + return a._inner if hasattr(a, "_inner") else a + + +def _arg_token(a): + """Runtime C base type of an argument, mirroring pymeos.factory's cdata + field-sniffing (no ffi object needed) plus Python scalars.""" + if isinstance(a, bool): + return "bool" + if isinstance(a, int): + return "int" + if isinstance(a, float): + return "float" + if isinstance(a, str): + return "text" + if isinstance(a, _dt.datetime): + return "timestamptz" + if isinstance(a, _dt.date): + return "date" + if isinstance(a, _dt.timedelta): + return "interval" + o = _unwrap(a) + for attr, tok in ( + ("temptype", "Temporal"), # every temporal subtype (6 families) + ("spansettype", "SpanSet"), + ("spantype", "Span"), + ("settype", "Set"), + ): + if hasattr(o, attr): + return tok + # Boxes / geo / spatial base types: distinguishing struct fields. + for attr, tok in ( + ("srid", "STBox"), # STBox carries an srid; TBox does not + ("period", "TBox"), + ("gflags", "GSERIALIZED"), + ("rid", "Npoint"), + ("radius", "Cbuffer"), + ): + if hasattr(o, attr): + return tok + return type(a).__name__ + + +def _wrap(res): + """Re-wrap MEOS-object results with the existing PyMEOS factories; pass + scalars (bool/float/int/str) and None straight through.""" + if res is None or isinstance(res, (bool, int, float, str, bytes)): + return res + from .factory import _TemporalFactory, _CollectionFactory + + for fn in (_TemporalFactory.create_temporal, + _CollectionFactory.create_collection): + try: + return fn(res) + except Exception: + pass + return res + + +def _dispatch(name, table, args): + key = tuple(_arg_token(a) for a in args) + sym = table.get(key) + if sym is None and len(key) == 2: # symmetric fallback + sym = table.get((key[1], key[0])) + if sym is None: + raise TypeError( + f"{name}(): no portable overload for argument types {key}; " + f"supported: {sorted(table)}" + ) + fn = getattr(_pymeos_cffi(), sym) + return _wrap(fn(*[_unwrap(a) for a in args])) +''' + +FUNC_TMPL = ''' + +_T_{bare} = {table!r} + + +def {bare}(*args): + """Portable ``{op}`` ({family}) -> pymeos_cffi backing call.""" + return _dispatch({bare!r}, _T_{bare}, args) +''' + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--headers", action="append", default=[], + help="MEOS include dir / header (repeatable)") + ap.add_argument("--cffi", action="store_true", + help="introspect the installed pymeos_cffi (CI gate)") + ap.add_argument("--contract", default=str(CONTRACT)) + ap.add_argument("--out", default=str(HERE.parents[1] / "pymeos" + / "portable.py")) + ap.add_argument("--report", default="") + ap.add_argument("--check", action="store_true", + help="exit non-zero unless 29/29 backed across 6 families") + args = ap.parse_args() + + idl = attach_portable_aliases({"functions": []}, Path(args.contract)) + pa = idl["portableAliases"] + by_bare = pa["byBareName"] # bareName -> operator + fam_of = {p["bareName"]: fam + for fam, lst in pa["families"].items() for p in lst} + explicit = pa.get("explicitBacking", {}) + in_scope = pa["scope"]["inScopeTypeFamilies"] + + # Codegen needs C signatures (to key dispatch on argument C types), so + # the universe is ALWAYS the MEOS headers. In CI the headers are the + # freshly built+installed MEOS at the install prefix, so the committed + # module stays in lock-step with the binding. --cffi adds a *separate* + # name-level assertion against the live built binding. + hdrs = args.headers or _default_header_dirs() + universe = universe_from_headers(hdrs) + src = f"headers:{hdrs}" + + # Parity (name-level, byte-identical to the MEOS-API SoT logic). + parity = build_parity( + attach_portable_aliases( + {"functions": [{"name": n} for n in universe]}, + Path(args.contract))) + + cffi_parity = None + if args.cffi: + try: + cffi_u = universe_from_cffi() + cffi_parity = build_parity(attach_portable_aliases( + {"functions": [{"name": n} for n in cffi_u]}, + Path(args.contract))) + except Exception as e: # binding not built + cffi_parity = {"error": str(e)} + + # Codegen tables: bare -> {(arg C-type tokens): symbol}. + fam_re = family_regexes(in_scope) + tables, family_witness = {}, defaultdict(set) + for bare in sorted(by_bare): + syms, _ = backing_symbols(bare, explicit.get(bare, []), universe) + tbl = {} + for s in syms: + for fam, rx in fam_re.items(): + if rx.search(s): + family_witness[fam].add(bare) + ret, params = universe.get(s, (None, None)) + if params is None: # cffi path: name only, no signature + continue + tbl[tuple(token(p) for p in params)] = s + tables[bare] = tbl + + backed = parity["backed"] + total = parity["total"] + missing_fam = [f for f in in_scope if not family_witness.get(f)] + + body = [HEADER, f"\n__all__ = {sorted(by_bare)!r}\n"] + for bare in sorted(by_bare): + body.append(FUNC_TMPL.format( + bare=bare, op=by_bare[bare], family=fam_of[bare], + table=tables[bare])) + Path(args.out).write_text("".join(body), encoding="utf-8") + + report = { + "source": src, + "universeSize": len(universe), + "parity": {k: parity[k] for k in + ("total", "backed", "needsExplicitBacking", "parityPct")}, + "unbacked": parity["unbacked"], + "inScopeFamilies": in_scope, + "familyWitness": {k: sorted(v) for k, v in family_witness.items()}, + "familiesMissing": missing_fam, + "cffiParity": (None if cffi_parity is None else + {k: cffi_parity.get(k) for k in + ("total", "backed", "needsExplicitBacking", + "unbacked", "error")}), + "out": args.out, + } + if args.report: + Path(args.report).parent.mkdir(parents=True, exist_ok=True) + Path(args.report).write_text(json.dumps(report, indent=2)) + + print(f"[portable] source={src} universe={len(universe)} " + f"backed={backed}/{total} ({parity['parityPct']}%) " + f"unbacked={parity['unbacked']} families_missing={missing_fam}") + print(f"[portable] wrote {args.out} ({len(by_bare)} bare names)") + + cffi_fail = False + if cffi_parity is not None: + if "error" in cffi_parity and cffi_parity.get("error"): + print(f"[portable] cffi check: binding not importable " + f"({cffi_parity['error']}) -- skipped") + else: + cffi_fail = (bool(cffi_parity["unbacked"]) + or cffi_parity["backed"] != cffi_parity["total"]) + print(f"[portable] cffi check: " + f"{cffi_parity['backed']}/{cffi_parity['total']} backed, " + f"unbacked={cffi_parity['unbacked']}") + + fail = (bool(parity["unbacked"]) or backed != total or bool(missing_fam) + or cffi_fail) + if args.check: + print("CHECK: " + ("FAIL" if fail else + f"PASS - {total}/{total} backed, 0 unbacked, " + f"all {len(in_scope)} families present")) + return 1 if fail else 0 + return 0 + + +def _default_header_dirs(): + """The single MEOS include dir to generate from, deterministically. + + Install-prefix first so a local run matches CI, where the workflow + builds + installs MEOS to the prefix (``/usr/local`` | ``/opt/homebrew``) + before regenerating; ``MEOS_INCLUDE`` overrides; the sibling MobilityDB + checkout is the last resort. A single dir keeps the committed artifact + reproducible across machines. + """ + for c in (os.environ.get("MEOS_INCLUDE"), + "/usr/local/include", "/opt/homebrew/include", + HERE.parents[2] / "MobilityDB" / "meos" / "include"): + if c and Path(c).is_dir() and any(Path(c).glob("meos*.h")): + return [str(c)] + return [] + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/portable_aliases/portable-aliases.json b/tools/portable_aliases/portable-aliases.json new file mode 100644 index 00000000..1cabac15 --- /dev/null +++ b/tools/portable_aliases/portable-aliases.json @@ -0,0 +1,60 @@ +{ + "_comment": "Canonical portable bare-name dialect — the single codegen source of truth (RFC #920). Every binding/engine generates the SAME bare names from this mapping so users learn one reference and assume the rest. Operators are SQL operator symbols; bareName is the portable function name. The mapping is type-agnostic: it applies to EVERY temporal type family.", + "provenance": { + "discussion": "MobilityDB#861", + "rfc": "MobilityDB RFC #920 (doc/rfc/sql-portability/README.md, branch rfc/sql-portability)", + "nativePR": "MobilityDB#1075 (1303 operator-overload aliases, each reusing the operator's own C symbol — identical by construction; CI-gated by tools/portable_aliases/generate.py --check)", + "manualChapter": "MobilityDB#1078" + }, + "families": { + "topology": [{"operator": "&&", "bareName": "overlaps"}, + {"operator": "@>", "bareName": "contains"}, + {"operator": "<@", "bareName": "contained"}, + {"operator": "-|-", "bareName": "adjacent"}], + "timePosition": [{"operator": "<<#", "bareName": "before"}, + {"operator": "#>>", "bareName": "after"}, + {"operator": "&<#", "bareName": "overbefore"}, + {"operator": "#&>", "bareName": "overafter"}], + "spaceX": [{"operator": "<<", "bareName": "left"}, + {"operator": ">>", "bareName": "right"}, + {"operator": "&<", "bareName": "overleft"}, + {"operator": "&>", "bareName": "overright"}], + "spaceY": [{"operator": "<<|", "bareName": "below"}, + {"operator": "|>>", "bareName": "above"}, + {"operator": "&<|", "bareName": "overbelow"}, + {"operator": "|&>", "bareName": "overabove"}], + "spaceZ": [{"operator": "<>", "bareName": "back"}, + {"operator": "&", "bareName": "overback"}], + "temporalComparison": [{"operator": "#=", "bareName": "teq"}, + {"operator": "#<>", "bareName": "tne"}, + {"operator": "#<", "bareName": "tlt"}, + {"operator": "#<=", "bareName": "tle"}, + {"operator": "#>", "bareName": "tgt"}, + {"operator": "#>=", "bareName": "tge"}], + "distance": [{"operator": "<->", "bareName": "tdistance"}, + {"operator": "|=|", "bareName": "nearestApproachDistance"}], + "same": [{"operator": "~=", "bareName": "same"}] + }, + "alreadyCanonical": [ + {"family": "ever", "operators": ["?="], "pattern": "ever_*"}, + {"family": "always", "operators": ["%="], "pattern": "always_*"}, + {"functions": ["eIntersects", "atTime", "restriction functions", + "spatial-relationship functions"]} + ], + "_explicitBackingComment": "Bare names whose MEOS C family prefix differs from the bare name itself. Verified against the catalog (not guessed): `nearestApproachDistance` is backed by the `nad_*` family (35 functions). Lets the parity audit resolve 100% honestly instead of false-flagging a real, present family.", + "explicitBacking": { + "nearestApproachDistance": ["nad"] + }, + "scope": { + "inScopeTypeFamilies": ["temporal", "geo", "cbuffer", "npoint", "pose", + "rgeo"], + "note": "cbuffer / npoint / pose / rgeo are FULL user-facing temporal types and ARE in scope — covered like every other type. PR #1075 already aliases all six families (1303 aliases). They must NOT be excluded from any parity headline; an upstream/audit note that 'defers' or 'jointly excludes' them is a known error being corrected — where another engine defers them, that is incomplete work to close (a gap with a plan), never an accepted exclusion." + }, + "notes": [ + "Generate aliases by reusing each operator's own backing C function (equivalence by construction), never by reimplementing; mirror MobilityDB tools/portable_aliases/generate.py + its 100%-coverage audit.", + "User-facing API uses the full name `trgeometry`; internal functions keep the `trgeo_` prefix — do NOT normalize the internal prefix.", + "Goal: 100% parity ecosystem-wide — every operator has its bare name on every engine, no gaps, no headline exclusions." + ] +} diff --git a/tools/portable_aliases/portable.py b/tools/portable_aliases/portable.py new file mode 100644 index 00000000..41398ac8 --- /dev/null +++ b/tools/portable_aliases/portable.py @@ -0,0 +1,45 @@ +"""Portable bare-name dialect — the single codegen source of truth. + +`meta/portable-aliases.json` is the curated, authoritative operator → +bare-name mapping (RFC #920; native in MobilityDB via PR #1075). Folding it +into the catalog means every binding/engine generates the *identical* bare +names, so a user learns one reference and assumes the rest. + +This is curated canonical data, not a heuristic — it is preserved verbatim +and only *derived* lookups are added (no guessing of C symbols: upstream +aliases reuse each operator's own backing function, equivalence by +construction). Pure dict → dict; no libclang. +""" + +import json +from pathlib import Path + + +def attach_portable_aliases(idl: dict, path: Path) -> dict: + """Attach ``idl["portableAliases"]`` from the canonical mapping file.""" + if not Path(path).exists(): + return idl + data = json.loads(Path(path).read_text()) + + pairs = [p for fam in data["families"].values() for p in fam] + by_operator = {p["operator"]: p["bareName"] for p in pairs} + by_bare_name = {p["bareName"]: p["operator"] for p in pairs} + + # Integrity: the mapping must be bijective (no operator or bare name may + # map two ways) — a collision would make codegen ambiguous. + if len(by_operator) != len(pairs) or len(by_bare_name) != len(pairs): + raise ValueError("portable-aliases: duplicate operator or bareName") + + idl["portableAliases"] = { + "provenance": data["provenance"], + "families": data["families"], + "alreadyCanonical": data["alreadyCanonical"], + "explicitBacking": data.get("explicitBacking", {}), + "scope": data["scope"], # cbuffer/npoint/pose/rgeo in scope + "notes": data["notes"], + "byOperator": by_operator, # "&&" -> "overlaps" + "byBareName": by_bare_name, # "overlaps" -> "&&" + "bareNames": sorted(by_bare_name), + "count": len(pairs), + } + return idl diff --git a/tools/portable_aliases/portable_parity.py b/tools/portable_aliases/portable_parity.py new file mode 100644 index 00000000..dc1f26b3 --- /dev/null +++ b/tools/portable_aliases/portable_parity.py @@ -0,0 +1,81 @@ +# Portable bare-name parity audit — the meos-api.json analogue of +# MobilityDB's `tools/portable_aliases/generate.py --check`. +# +# python run.py # catalog with `portableAliases` + functions +# python portable_parity.py # -> output/meos-portable-parity.json +# +# For every canonical bare name (PR #8 / RFC #920) it reports the catalog +# function family that backs it, by the MEOS bare-name prefix convention +# (`overlaps_*`, `teq_*`, `same_*`, …). A bare name with no prefix match is +# **not** asserted to be an API gap (some map through a different C prefix, +# e.g. `nearestApproachDistance` ↔ `nad_*`): it is flagged +# `needs-explicit-backing` so the cross-repo work can add an explicit +# operator→C-family entry — an honest signal, never a fabricated verdict. + +import json +import sys +from pathlib import Path + +IN_PATH = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("output/meos-idl.json") +OUT_PATH = (Path(sys.argv[2]) if len(sys.argv) > 2 + else Path("output/meos-portable-parity.json")) + + +def build_parity(catalog: dict) -> dict: + pa = catalog.get("portableAliases") + if not pa: + raise ValueError("catalog has no `portableAliases` — run run.py") + fam_of = {p["bareName"]: (fam, p["operator"]) + for fam, lst in pa["families"].items() for p in lst} + explicit = pa.get("explicitBacking", {}) + names = [f["name"] for f in catalog.get("functions", [])] + + def _matches(prefix): + return [n for n in names + if n == prefix or n.startswith(prefix + "_")] + + by_bare = {} + for bare, (fam, op) in sorted(fam_of.items()): + hits, via = _matches(bare), "prefix" + if not hits: # try the verified explicit map + for pref in explicit.get(bare, []): + hits += _matches(pref) + via = "explicit" if hits else None + by_bare[bare] = { + "operator": op, "family": fam, "via": via, + "backedBy": len(hits), + "sample": sorted(hits)[:3], + "status": "backed" if hits else "needs-explicit-backing", + } + backed = [b for b, v in by_bare.items() if v["status"] == "backed"] + unbacked = sorted(b for b, v in by_bare.items() + if v["status"] == "needs-explicit-backing") + total = len(by_bare) + return { + "total": total, + "backed": len(backed), + "needsExplicitBacking": len(unbacked), + "parityPct": round(len(backed) * 100 / total, 1) if total else 0, + "unbacked": unbacked, # the precise cross-repo worklist + "byBareName": by_bare, + } + + +def main() -> None: + if not IN_PATH.exists(): + sys.exit(f"Catalog not found: {IN_PATH} — run `python run.py` first.") + rep = build_parity(json.loads(IN_PATH.read_text())) + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUT_PATH.write_text(json.dumps(rep, indent=2)) + print(f"[portable-parity] {rep['backed']}/{rep['total']} bare names " + f"backed in the catalog ({rep['parityPct']}%); " + f"{rep['needsExplicitBacking']} need an explicit backing entry " + f"→ {OUT_PATH}", file=sys.stderr) + for b in rep["unbacked"]: + v = rep["byBareName"][b] + print(f" needs-explicit-backing: {b!r} ({v['operator']}, " + f"{v['family']})", file=sys.stderr) + + +if __name__ == "__main__": + main()