small-cache 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- small_cache-0.0.1/.github/dependabot.yml +11 -0
- small_cache-0.0.1/.github/workflows/pip.yml +35 -0
- small_cache-0.0.1/.github/workflows/wheels.yml +72 -0
- small_cache-0.0.1/.gitignore +9 -0
- small_cache-0.0.1/CMakeLists.txt +131 -0
- small_cache-0.0.1/PKG-INFO +70 -0
- small_cache-0.0.1/README.md +60 -0
- small_cache-0.0.1/pyproject.toml +44 -0
- small_cache-0.0.1/src/small_cache/__init__.py +1 -0
- small_cache-0.0.1/src/small_cache.cpp +437 -0
- small_cache-0.0.1/tests/test_basic.py +5 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Pip
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
name: Build with Pip
|
|
9
|
+
runs-on: ${{ matrix.platform }}
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
platform: [windows-latest, macos-latest, ubuntu-latest]
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Set min macOS version
|
|
24
|
+
if: runner.os == 'Linux'
|
|
25
|
+
run: |
|
|
26
|
+
echo "CC=clang" >> $GITHUB_ENV
|
|
27
|
+
echo "CXX=clang++" >> $GITHUB_ENV
|
|
28
|
+
|
|
29
|
+
- name: Build and install
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install pytest
|
|
32
|
+
pip install --verbose .
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: python -m pytest
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: Wheels
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types:
|
|
7
|
+
- published
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build_sdist:
|
|
11
|
+
name: Build SDist
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
submodules: true
|
|
17
|
+
|
|
18
|
+
- name: Build SDist
|
|
19
|
+
run: pipx run build --sdist
|
|
20
|
+
|
|
21
|
+
- name: Check metadata
|
|
22
|
+
run: pipx run twine check dist/*
|
|
23
|
+
|
|
24
|
+
- uses: actions/upload-artifact@v4
|
|
25
|
+
with:
|
|
26
|
+
name: dist-sdist
|
|
27
|
+
path: dist/*.tar.gz
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
build_wheels:
|
|
31
|
+
name: Wheels on ${{ matrix.os }}
|
|
32
|
+
runs-on: ${{ matrix.os }}
|
|
33
|
+
strategy:
|
|
34
|
+
fail-fast: false
|
|
35
|
+
matrix:
|
|
36
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
submodules: true
|
|
42
|
+
|
|
43
|
+
- uses: pypa/cibuildwheel@v3.0
|
|
44
|
+
|
|
45
|
+
- name: Verify clean directory
|
|
46
|
+
run: git diff --exit-code
|
|
47
|
+
shell: bash
|
|
48
|
+
|
|
49
|
+
- name: Upload wheels
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
path: wheelhouse/*.whl
|
|
53
|
+
name: dist-${{ matrix.os }}
|
|
54
|
+
|
|
55
|
+
upload_all:
|
|
56
|
+
name: Upload if release
|
|
57
|
+
needs: [build_wheels, build_sdist]
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
if: github.event_name == 'release' && github.event.action == 'published'
|
|
60
|
+
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/setup-python@v5
|
|
63
|
+
- uses: actions/download-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
path: dist
|
|
66
|
+
pattern: dist-*
|
|
67
|
+
merge-multiple: true
|
|
68
|
+
|
|
69
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
user: __token__
|
|
72
|
+
password: ${{ secrets.pypi_password }}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15...3.26)
|
|
2
|
+
|
|
3
|
+
project(small_cache LANGUAGES CXX)
|
|
4
|
+
|
|
5
|
+
set(CMAKE_CXX_STANDARD 23)
|
|
6
|
+
set(CXX_STANDARD_REQUIRED On)
|
|
7
|
+
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
|
|
8
|
+
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
|
|
9
|
+
set(BUILD_SHARED_LIBS OFF)
|
|
10
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
11
|
+
|
|
12
|
+
if (NOT SKBUILD)
|
|
13
|
+
message(WARNING "\
|
|
14
|
+
This CMake file is meant to be executed using 'scikit-build'. Running
|
|
15
|
+
it directly will almost certainly not produce the desired result. If
|
|
16
|
+
you are a user trying to install this package, please use the command
|
|
17
|
+
below, which will install all necessary build dependencies, compile
|
|
18
|
+
the package in an isolated environment, and then install it.
|
|
19
|
+
=====================================================================
|
|
20
|
+
$ pip install .
|
|
21
|
+
=====================================================================
|
|
22
|
+
If you are a software developer, and this is your own package, then
|
|
23
|
+
it is usually much more efficient to install the build dependencies
|
|
24
|
+
in your environment once and use the following command that avoids
|
|
25
|
+
a costly creation of a new virtual environment at every compilation:
|
|
26
|
+
=====================================================================
|
|
27
|
+
$ pip install nanobind scikit-build-core[pyproject]
|
|
28
|
+
$ pip install --no-build-isolation -ve .
|
|
29
|
+
=====================================================================
|
|
30
|
+
You may optionally add -Ceditable.rebuild=true to auto-rebuild when
|
|
31
|
+
the package is imported. Otherwise, you need to re-run the above
|
|
32
|
+
after editing C++ files.")
|
|
33
|
+
endif ()
|
|
34
|
+
|
|
35
|
+
# Try to import all Python components potentially needed by nanobind
|
|
36
|
+
find_package(Python 3.11
|
|
37
|
+
REQUIRED COMPONENTS Interpreter Development.Module
|
|
38
|
+
OPTIONAL_COMPONENTS Development.SABIModule)
|
|
39
|
+
|
|
40
|
+
# Import nanobind through CMake's find_package mechanism
|
|
41
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
42
|
+
|
|
43
|
+
# We are now ready to compile the actual extension module
|
|
44
|
+
nanobind_add_module(
|
|
45
|
+
# Name of the extension
|
|
46
|
+
_small_cache_impl
|
|
47
|
+
|
|
48
|
+
# Target the stable ABI for Python 3.12+, which reduces
|
|
49
|
+
# the number of binary wheels that must be built. This
|
|
50
|
+
# does nothing on older Python versions
|
|
51
|
+
STABLE_ABI
|
|
52
|
+
|
|
53
|
+
# Build libnanobind statically and merge it into the
|
|
54
|
+
# extension (which itself remains a shared library)
|
|
55
|
+
#
|
|
56
|
+
# If your project builds multiple extensions, you can
|
|
57
|
+
# replace this flag by NB_SHARED to conserve space by
|
|
58
|
+
# reusing a shared libnanobind across libraries
|
|
59
|
+
NB_STATIC
|
|
60
|
+
|
|
61
|
+
# Source code goes here
|
|
62
|
+
src/small_cache.cpp
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
include(FetchContent)
|
|
66
|
+
FetchContent_Declare(
|
|
67
|
+
glaze
|
|
68
|
+
GIT_REPOSITORY https://github.com/stephenberry/glaze.git
|
|
69
|
+
GIT_TAG main
|
|
70
|
+
GIT_SHALLOW TRUE
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
FetchContent_MakeAvailable(glaze)
|
|
74
|
+
|
|
75
|
+
FetchContent_Declare(
|
|
76
|
+
tsl_sparse_map
|
|
77
|
+
GIT_REPOSITORY https://github.com/Tessil/sparse-map
|
|
78
|
+
GIT_TAG master
|
|
79
|
+
GIT_SHALLOW TRUE
|
|
80
|
+
)
|
|
81
|
+
FetchContent_MakeAvailable(tsl_sparse_map)
|
|
82
|
+
|
|
83
|
+
FetchContent_Declare(
|
|
84
|
+
absl
|
|
85
|
+
GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
|
|
86
|
+
GIT_TAG master
|
|
87
|
+
GIT_SHALLOW TRUE
|
|
88
|
+
OVERRIDE_FIND_PACKAGE TRUE
|
|
89
|
+
EXCLUDE_FROM_ALL
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
find_package(
|
|
93
|
+
absl
|
|
94
|
+
REQUIRED
|
|
95
|
+
COMPONENTS flat_hash_map hash
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
Set(FETCHCONTENT_QUIET FALSE)
|
|
99
|
+
FetchContent_Declare(
|
|
100
|
+
Boost
|
|
101
|
+
URL https://github.com/boostorg/boost/releases/download/boost-1.88.0/boost-1.88.0-cmake.7z
|
|
102
|
+
GIT_TAG "boost-1.88.0"
|
|
103
|
+
USES_TERMINAL_DOWNLOAD TRUE
|
|
104
|
+
GIT_PROGRESS TRUE
|
|
105
|
+
DOWNLOAD_NO_EXTRACT FALSE
|
|
106
|
+
OVERRIDE_FIND_PACKAGE TRUE
|
|
107
|
+
EXCLUDE_FROM_ALL
|
|
108
|
+
)
|
|
109
|
+
Set(FETCHCONTENT_QUIET TRUE)
|
|
110
|
+
|
|
111
|
+
find_package(
|
|
112
|
+
Boost
|
|
113
|
+
1.88.0
|
|
114
|
+
EXACT # Minimum or EXACT version e.g. 1.86.0
|
|
115
|
+
REQUIRED # Fail with error if Boost is not found
|
|
116
|
+
COMPONENTS flyweight
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
target_link_libraries(
|
|
120
|
+
_small_cache_impl
|
|
121
|
+
PRIVATE
|
|
122
|
+
glaze::glaze
|
|
123
|
+
tsl::sparse_map
|
|
124
|
+
absl::flat_hash_map
|
|
125
|
+
absl::hash
|
|
126
|
+
Boost::flyweight
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Install directive for scikit-build-core
|
|
131
|
+
install(TARGETS _small_cache_impl LIBRARY DESTINATION small_cache)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: small-cache
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Small cache
|
|
5
|
+
Author: kam1k4dze
|
|
6
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
7
|
+
Project-URL: Homepage, https://github.com/kam1k4dze/small_cache
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
small_cache
|
|
12
|
+
================
|
|
13
|
+
|
|
14
|
+
| CI | status |
|
|
15
|
+
|----------------------|--------|
|
|
16
|
+
| pip builds | [![Pip Action Status][actions-pip-badge]][actions-pip-link] |
|
|
17
|
+
| wheels | [![Wheel Action Status][actions-wheels-badge]][actions-wheels-link] |
|
|
18
|
+
|
|
19
|
+
[actions-pip-link]: https://github.com/wjakob/small_cache/actions?query=workflow%3APip
|
|
20
|
+
[actions-pip-badge]: https://github.com/wjakob/small_cache/workflows/Pip/badge.svg
|
|
21
|
+
[actions-wheels-link]: https://github.com/wjakob/small_cache/actions?query=workflow%3AWheels
|
|
22
|
+
[actions-wheels-badge]: https://github.com/wjakob/small_cache/workflows/Wheels/badge.svg
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
This repository contains a tiny project showing how to create C++ bindings
|
|
26
|
+
using [nanobind](https://github.com/wjakob/nanobind) and
|
|
27
|
+
[scikit-build-core](https://scikit-build-core.readthedocs.io/en/latest/index.html). It
|
|
28
|
+
was derived from the corresponding _pybind11_ [example
|
|
29
|
+
project](https://github.com/pybind/scikit_build_example/) developed by
|
|
30
|
+
[@henryiii](https://github.com/henryiii).
|
|
31
|
+
|
|
32
|
+
Furthermore, the [bazel](https://github.com/wjakob/small_cache/tree/bazel) branch contains an example
|
|
33
|
+
on how to build nanobind bindings extensions with Bazel using the [nanobind-bazel](https://github.com/nicholasjng/nanobind-bazel/) project.
|
|
34
|
+
|
|
35
|
+
Installation
|
|
36
|
+
------------
|
|
37
|
+
|
|
38
|
+
1. Clone this repository
|
|
39
|
+
2. Run `pip install ./small_cache`
|
|
40
|
+
|
|
41
|
+
Afterwards, you should be able to issue the following commands (shown in an
|
|
42
|
+
interactive Python session):
|
|
43
|
+
|
|
44
|
+
```pycon
|
|
45
|
+
>>> import small_cache
|
|
46
|
+
>>> small_cache.add(1, 2)
|
|
47
|
+
3
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
CI Examples
|
|
51
|
+
-----------
|
|
52
|
+
|
|
53
|
+
The `.github/workflows` directory contains two continuous integration workflows
|
|
54
|
+
for GitHub Actions. The first one (`pip`) runs automatically after each commit
|
|
55
|
+
and ensures that packages can be built successfully and that tests pass.
|
|
56
|
+
|
|
57
|
+
The `wheels` workflow uses
|
|
58
|
+
[cibuildwheel](https://cibuildwheel.readthedocs.io/en/stable/) to automatically
|
|
59
|
+
produce binary wheels for a large variety of platforms. If a `pypi_password`
|
|
60
|
+
token is provided using GitHub Action's _secrets_ feature, this workflow can
|
|
61
|
+
even automatically upload packages on PyPI.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
License
|
|
65
|
+
-------
|
|
66
|
+
|
|
67
|
+
_nanobind_ and this example repository are both provided under a BSD-style
|
|
68
|
+
license that can be found in the [LICENSE](./LICENSE) file. By using,
|
|
69
|
+
distributing, or contributing to this project, you agree to the terms and
|
|
70
|
+
conditions of this license.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
small_cache
|
|
2
|
+
================
|
|
3
|
+
|
|
4
|
+
| CI | status |
|
|
5
|
+
|----------------------|--------|
|
|
6
|
+
| pip builds | [![Pip Action Status][actions-pip-badge]][actions-pip-link] |
|
|
7
|
+
| wheels | [![Wheel Action Status][actions-wheels-badge]][actions-wheels-link] |
|
|
8
|
+
|
|
9
|
+
[actions-pip-link]: https://github.com/wjakob/small_cache/actions?query=workflow%3APip
|
|
10
|
+
[actions-pip-badge]: https://github.com/wjakob/small_cache/workflows/Pip/badge.svg
|
|
11
|
+
[actions-wheels-link]: https://github.com/wjakob/small_cache/actions?query=workflow%3AWheels
|
|
12
|
+
[actions-wheels-badge]: https://github.com/wjakob/small_cache/workflows/Wheels/badge.svg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
This repository contains a tiny project showing how to create C++ bindings
|
|
16
|
+
using [nanobind](https://github.com/wjakob/nanobind) and
|
|
17
|
+
[scikit-build-core](https://scikit-build-core.readthedocs.io/en/latest/index.html). It
|
|
18
|
+
was derived from the corresponding _pybind11_ [example
|
|
19
|
+
project](https://github.com/pybind/scikit_build_example/) developed by
|
|
20
|
+
[@henryiii](https://github.com/henryiii).
|
|
21
|
+
|
|
22
|
+
Furthermore, the [bazel](https://github.com/wjakob/small_cache/tree/bazel) branch contains an example
|
|
23
|
+
on how to build nanobind bindings extensions with Bazel using the [nanobind-bazel](https://github.com/nicholasjng/nanobind-bazel/) project.
|
|
24
|
+
|
|
25
|
+
Installation
|
|
26
|
+
------------
|
|
27
|
+
|
|
28
|
+
1. Clone this repository
|
|
29
|
+
2. Run `pip install ./small_cache`
|
|
30
|
+
|
|
31
|
+
Afterwards, you should be able to issue the following commands (shown in an
|
|
32
|
+
interactive Python session):
|
|
33
|
+
|
|
34
|
+
```pycon
|
|
35
|
+
>>> import small_cache
|
|
36
|
+
>>> small_cache.add(1, 2)
|
|
37
|
+
3
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
CI Examples
|
|
41
|
+
-----------
|
|
42
|
+
|
|
43
|
+
The `.github/workflows` directory contains two continuous integration workflows
|
|
44
|
+
for GitHub Actions. The first one (`pip`) runs automatically after each commit
|
|
45
|
+
and ensures that packages can be built successfully and that tests pass.
|
|
46
|
+
|
|
47
|
+
The `wheels` workflow uses
|
|
48
|
+
[cibuildwheel](https://cibuildwheel.readthedocs.io/en/stable/) to automatically
|
|
49
|
+
produce binary wheels for a large variety of platforms. If a `pypi_password`
|
|
50
|
+
token is provided using GitHub Action's _secrets_ feature, this workflow can
|
|
51
|
+
even automatically upload packages on PyPI.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
License
|
|
55
|
+
-------
|
|
56
|
+
|
|
57
|
+
_nanobind_ and this example repository are both provided under a BSD-style
|
|
58
|
+
license that can be found in the [LICENSE](./LICENSE) file. By using,
|
|
59
|
+
distributing, or contributing to this project, you agree to the terms and
|
|
60
|
+
conditions of this license.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["scikit-build-core >=0.10", "nanobind >=1.3.2"]
|
|
3
|
+
build-backend = "scikit_build_core.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "small-cache"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Small cache"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "kam1k4dze"},
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: BSD License",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/kam1k4dze/small_cache"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.scikit-build]
|
|
23
|
+
# Protect the configuration against future changes in scikit-build-core
|
|
24
|
+
minimum-version = "build-system.requires"
|
|
25
|
+
|
|
26
|
+
# Setuptools-style build caching in a local directory
|
|
27
|
+
build-dir = "build/{wheel_tag}"
|
|
28
|
+
|
|
29
|
+
# Build stable ABI wheels for CPython 3.12+
|
|
30
|
+
wheel.py-api = "cp312"
|
|
31
|
+
|
|
32
|
+
[tool.cibuildwheel]
|
|
33
|
+
# Necessary to see build output from the actual compilation
|
|
34
|
+
build-verbosity = 1
|
|
35
|
+
|
|
36
|
+
archs = ["auto64"]
|
|
37
|
+
|
|
38
|
+
# Run pytest to ensure that the package was correctly built
|
|
39
|
+
test-command = "pytest {project}/tests"
|
|
40
|
+
test-requires = "pytest"
|
|
41
|
+
|
|
42
|
+
# Needed for full C++17 support
|
|
43
|
+
[tool.cibuildwheel.macos.environment]
|
|
44
|
+
MACOSX_DEPLOYMENT_TARGET = "10.14"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._small_cache_impl import SmallCache, __doc__
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#include <nanobind/nanobind.h>
|
|
2
|
+
#include <nanobind/stl/variant.h>
|
|
3
|
+
#include <nanobind/stl/string.h>
|
|
4
|
+
#include <nanobind/stl/vector.h>
|
|
5
|
+
#include <nanobind/stl/pair.h>
|
|
6
|
+
#include <nanobind/stl/optional.h>
|
|
7
|
+
#include <nanobind/stl/unordered_map.h>
|
|
8
|
+
#include <tsl/sparse_map.h>
|
|
9
|
+
#include <absl/hash/hash.h>
|
|
10
|
+
#include <absl/container/flat_hash_map.h>
|
|
11
|
+
#include <boost/flyweight.hpp>
|
|
12
|
+
#include <optional>
|
|
13
|
+
#include <algorithm>
|
|
14
|
+
#include <variant>
|
|
15
|
+
#include <vector>
|
|
16
|
+
#include <string>
|
|
17
|
+
#include <glaze/glaze.hpp>
|
|
18
|
+
|
|
19
|
+
namespace json {
|
|
20
|
+
using AttributeValue = std::variant<bool, double, std::string,
|
|
21
|
+
std::vector<glz::raw_json>,
|
|
22
|
+
std::optional<glz::raw_json> >;
|
|
23
|
+
|
|
24
|
+
struct Attribute {
|
|
25
|
+
std::string id{};
|
|
26
|
+
AttributeValue value{};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
struct Item {
|
|
30
|
+
std::string id{};
|
|
31
|
+
std::vector<Attribute> attributes{};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
struct Pagination {
|
|
35
|
+
int page{};
|
|
36
|
+
int pages{};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
struct Result {
|
|
40
|
+
int count{};
|
|
41
|
+
Pagination pagination{};
|
|
42
|
+
std::vector<Item> data{};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
struct Response {
|
|
46
|
+
Result result{};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
namespace nb = nanobind;
|
|
52
|
+
using namespace nb::literals;
|
|
53
|
+
|
|
54
|
+
class SmallCache {
|
|
55
|
+
using str = std::string;
|
|
56
|
+
using strVec = std::vector<str>;
|
|
57
|
+
using fwStr = boost::flyweight<str>;
|
|
58
|
+
using strVecUPtr = std::unique_ptr<std::vector<fwStr> >;
|
|
59
|
+
|
|
60
|
+
using AttributeValue = std::variant<std::monostate, double, bool, fwStr, strVecUPtr>;
|
|
61
|
+
using pyAttrValue = std::variant<std::monostate, bool, double, str, strVec>;
|
|
62
|
+
|
|
63
|
+
public:
|
|
64
|
+
explicit SmallCache(const strVec &attributes) : numberOfAttributes(attributes.size()) {
|
|
65
|
+
if (attributes.empty()) {
|
|
66
|
+
throw std::runtime_error("No attributes provided");
|
|
67
|
+
}
|
|
68
|
+
if (attributes.size() > 255) {
|
|
69
|
+
throw std::runtime_error("Too many attributes provided");
|
|
70
|
+
}
|
|
71
|
+
attrMap.reserve(attributes.size());
|
|
72
|
+
attrIdx.reserve(attributes.size());
|
|
73
|
+
for (size_t idx = 0; idx < attributes.size(); ++idx) {
|
|
74
|
+
const auto &attr = attributes[idx];
|
|
75
|
+
attrIdx.emplace_back(attr);
|
|
76
|
+
attrMap.emplace(attr, idx);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
SmallCache(const SmallCache &) = delete;
|
|
81
|
+
|
|
82
|
+
SmallCache &operator=(const SmallCache &) = delete;
|
|
83
|
+
|
|
84
|
+
struct MarkedItem {
|
|
85
|
+
bool isNew = true;
|
|
86
|
+
std::array<uint32_t, 3> attrs_flags{}; // 96 bits total
|
|
87
|
+
std::vector<AttributeValue> value;
|
|
88
|
+
|
|
89
|
+
[[nodiscard]] std::vector<size_t> getIdxs() const {
|
|
90
|
+
std::vector<size_t> idxs;
|
|
91
|
+
idxs.reserve(value.size());
|
|
92
|
+
|
|
93
|
+
for (size_t w = 0; w < attrs_flags.size(); ++w) {
|
|
94
|
+
uint32_t bits = attrs_flags[w];
|
|
95
|
+
while (bits) {
|
|
96
|
+
unsigned b = std::countr_zero(bits);
|
|
97
|
+
idxs.push_back(w * 32 + b);
|
|
98
|
+
bits &= bits - 1; // clear that bit
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return idxs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[[nodiscard]] constexpr bool hasIdx(size_t idx) const noexcept {
|
|
105
|
+
if (idx >= attrs_flags.size() * 32)
|
|
106
|
+
return false; // out of range
|
|
107
|
+
auto w = idx / 32;
|
|
108
|
+
auto b = idx % 32;
|
|
109
|
+
return (attrs_flags[w] >> b) & 1u;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[[nodiscard]] std::optional<std::reference_wrapper<AttributeValue> > getValue(size_t idx) noexcept {
|
|
113
|
+
if (!hasIdx(idx))
|
|
114
|
+
return std::nullopt;
|
|
115
|
+
|
|
116
|
+
// count how many bits are set before 'idx'
|
|
117
|
+
size_t w = idx / 32, b = idx % 32;
|
|
118
|
+
size_t pos = 0;
|
|
119
|
+
|
|
120
|
+
// sum full words
|
|
121
|
+
for (size_t i = 0; i < w; ++i) {
|
|
122
|
+
pos += std::popcount(attrs_flags[i]);
|
|
123
|
+
}
|
|
124
|
+
// sum lower bits in the same word
|
|
125
|
+
if (b > 0) {
|
|
126
|
+
uint32_t mask = (1u << b) - 1;
|
|
127
|
+
pos += std::popcount(attrs_flags[w] & mask);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return value.size() > pos ? std::optional{std::ref(value[pos])} : std::nullopt; // just-in-case
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[[nodiscard]] std::optional<std::reference_wrapper<const AttributeValue> > getValue(size_t idx) const noexcept {
|
|
134
|
+
if (auto ref = const_cast<MarkedItem *>(this)->getValue(idx))
|
|
135
|
+
return std::cref(ref->get());
|
|
136
|
+
return std::nullopt;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
void setMarkedItem(MarkedItem &item, const std::unordered_map<str, pyAttrValue> &attrs) {
|
|
141
|
+
item.isNew = true;
|
|
142
|
+
item.attrs_flags.fill(0);
|
|
143
|
+
|
|
144
|
+
// build a slot for each possible attr index
|
|
145
|
+
std::vector<std::optional<AttributeValue> > slots(attrMap.size());
|
|
146
|
+
|
|
147
|
+
// 1) collect into slots[] by index
|
|
148
|
+
for (auto &[name, pyVal]: attrs) {
|
|
149
|
+
if (auto it = attrMap.find(name); it != attrMap.end()) {
|
|
150
|
+
slots[it->second] = convert_value(pyVal);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2) reserve exactly as many as we’ll push
|
|
155
|
+
auto count = std::ranges::count_if(slots, [](auto &o) { return o.has_value(); });
|
|
156
|
+
item.value.clear();
|
|
157
|
+
item.value.reserve(count);
|
|
158
|
+
|
|
159
|
+
// 3) walk slots in ascending idx order,
|
|
160
|
+
// set flags and move values into item.value
|
|
161
|
+
for (size_t idx = 0; idx < slots.size(); ++idx) {
|
|
162
|
+
if (auto &opt = slots[idx]; opt) {
|
|
163
|
+
item.value.push_back(std::move(*opt));
|
|
164
|
+
auto w = idx / 32;
|
|
165
|
+
auto b = idx % 32;
|
|
166
|
+
item.attrs_flags[w] |= (1u << b);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
struct getItemResponse {
|
|
173
|
+
std::vector<pyAttrValue> attribute_values;
|
|
174
|
+
std::vector<pyAttrValue> attribute_names;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
template<class... Ts>
|
|
179
|
+
struct overloaded : Ts... {
|
|
180
|
+
using Ts::operator()...;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
template<class... Ts>
|
|
184
|
+
overloaded(Ts...) -> overloaded<Ts...>;
|
|
185
|
+
|
|
186
|
+
static AttributeValue convert_value(const json::AttributeValue &src) {
|
|
187
|
+
return std::visit(overloaded{
|
|
188
|
+
[](bool b) -> AttributeValue { return b; },
|
|
189
|
+
[](double d) -> AttributeValue { return d; },
|
|
190
|
+
[](const str &s) -> AttributeValue {
|
|
191
|
+
// if (s.empty()) return std::monostate{};
|
|
192
|
+
return fwStr{s};
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
[](const std::vector<glz::raw_json> &json_vec) -> AttributeValue {
|
|
196
|
+
auto out = std::make_unique<std::vector<fwStr> >();
|
|
197
|
+
out->reserve(json_vec.size());
|
|
198
|
+
for (auto &r: json_vec)
|
|
199
|
+
if (!r.str.empty())
|
|
200
|
+
out->emplace_back(r.str);
|
|
201
|
+
// if (out->empty()) {
|
|
202
|
+
// return std::monostate{};
|
|
203
|
+
// }
|
|
204
|
+
out->shrink_to_fit();
|
|
205
|
+
return out;
|
|
206
|
+
},
|
|
207
|
+
[](const std::optional<glz::raw_json> &o) -> AttributeValue {
|
|
208
|
+
// if (!o.has_value() || o->str.empty()) return std::monostate{};
|
|
209
|
+
return fwStr{o->str};
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
},
|
|
213
|
+
src);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
static pyAttrValue convert_valueJ(const json::AttributeValue &src) {
|
|
218
|
+
return std::visit(overloaded{
|
|
219
|
+
[](std::monostate b) -> pyAttrValue { return b; },
|
|
220
|
+
[](bool b) -> pyAttrValue { return b; },
|
|
221
|
+
[](double d) -> pyAttrValue { return d; },
|
|
222
|
+
[](const str &s) -> pyAttrValue { return s; },
|
|
223
|
+
[](const std::vector<glz::raw_json> &json_vec) -> pyAttrValue {
|
|
224
|
+
return json_vec | std::views::transform([](const glz::raw_json &j) -> str {
|
|
225
|
+
return j.str;
|
|
226
|
+
}) |
|
|
227
|
+
std::ranges::to<strVec>();
|
|
228
|
+
},
|
|
229
|
+
[](const std::optional<glz::raw_json> &o) -> pyAttrValue { return o ? o->str : ""; },
|
|
230
|
+
|
|
231
|
+
},
|
|
232
|
+
src);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static pyAttrValue convert_value(const AttributeValue &src) {
|
|
236
|
+
return std::visit(overloaded{
|
|
237
|
+
[](std::monostate b) -> pyAttrValue { return b; },
|
|
238
|
+
[](bool b) -> pyAttrValue { return b; },
|
|
239
|
+
[](double d) -> pyAttrValue { return d; },
|
|
240
|
+
[](const fwStr &s) -> pyAttrValue { return s; },
|
|
241
|
+
[](const strVecUPtr &fw_vec) -> pyAttrValue {
|
|
242
|
+
return *fw_vec | std::views::transform([](const fwStr &s) -> str { return s; }) |
|
|
243
|
+
std::ranges::to<strVec>();
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
src);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static AttributeValue convert_value(const pyAttrValue &src) {
|
|
250
|
+
return std::visit(overloaded{
|
|
251
|
+
[](std::monostate b) -> AttributeValue { return b; },
|
|
252
|
+
[](bool b) -> AttributeValue { return b; },
|
|
253
|
+
[](double d) -> AttributeValue { return d; },
|
|
254
|
+
[](const str &s) -> AttributeValue { return fwStr{s}; },
|
|
255
|
+
[](const strVec &vec) -> AttributeValue {
|
|
256
|
+
auto out = std::make_unique<std::vector<fwStr> >();
|
|
257
|
+
out->reserve(vec.size());
|
|
258
|
+
for (auto &r: vec) {
|
|
259
|
+
out->emplace_back(r);
|
|
260
|
+
}
|
|
261
|
+
out->shrink_to_fit();
|
|
262
|
+
return out;
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
src);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static str to_string(const pyAttrValue &src) {
|
|
269
|
+
return std::visit(overloaded{
|
|
270
|
+
[](std::monostate) -> str { return "null"; },
|
|
271
|
+
[](bool b) -> str { return b ? "true" : "false"; },
|
|
272
|
+
[](double d) -> str { return std::to_string(d); },
|
|
273
|
+
[](const str &s) -> str { return s; },
|
|
274
|
+
|
|
275
|
+
[](const strVec &vecptr) -> str {
|
|
276
|
+
str out = "[";
|
|
277
|
+
|
|
278
|
+
for (auto &r: vecptr) {
|
|
279
|
+
out.append(r);
|
|
280
|
+
out.append(",");
|
|
281
|
+
}
|
|
282
|
+
out.append("]");
|
|
283
|
+
return out;
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
src);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
void add_item(const str &item_id, const std::unordered_map<str, pyAttrValue> &attributes) {
|
|
290
|
+
if (!transactionOpened) {
|
|
291
|
+
throw std::runtime_error("Transaction not opened");
|
|
292
|
+
}
|
|
293
|
+
auto &marked_attrs = cache[item_id];
|
|
294
|
+
setMarkedItem(marked_attrs, attributes);
|
|
295
|
+
increaseIteratorVersion();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
std::vector<pyAttrValue> get(const str &id, const strVec &attributes) {
|
|
299
|
+
if (attributes.empty()) {
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
if (cache.contains(id)) {
|
|
303
|
+
const auto &item = cache.at(id);
|
|
304
|
+
return attributes | std::views::transform([this, &item](const auto &attr_name) -> pyAttrValue {
|
|
305
|
+
if (!attrMap.contains(attr_name))
|
|
306
|
+
// throw std::runtime_error("Attribute " + attr_name + " does not exist in cache");
|
|
307
|
+
return {};
|
|
308
|
+
const auto attr_idx = attrMap.at(attr_name);
|
|
309
|
+
const auto attr_value = item.getValue(attr_idx);
|
|
310
|
+
if (!attr_value)
|
|
311
|
+
return {};
|
|
312
|
+
return convert_value(*attr_value);
|
|
313
|
+
}) |
|
|
314
|
+
std::ranges::to<std::vector<pyAttrValue> >();
|
|
315
|
+
}
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
struct getAllResponse {
|
|
321
|
+
uint64_t iterator_version{};
|
|
322
|
+
uint64_t iterator{};
|
|
323
|
+
std::vector<std::pair<str, std::vector<pyAttrValue> > > result{};
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
str get_all(const strVec &attributes, uint64_t per_iteration = 10000, uint64_t iterator_version = 0,
|
|
327
|
+
uint64_t iterator = 0) {
|
|
328
|
+
if (transactionOpened)
|
|
329
|
+
throw std::runtime_error("Can't get all when transaction is opened");
|
|
330
|
+
if (iterator && iterator_version == 0) {
|
|
331
|
+
throw std::runtime_error("Invalid iterator version");
|
|
332
|
+
}
|
|
333
|
+
if (iterator && iterator_version != this->iterator_version)
|
|
334
|
+
throw std::runtime_error("Iterator already invalidated");
|
|
335
|
+
getAllResponse resp{};
|
|
336
|
+
std::vector<str> keys = cache | std::views::keys | std::ranges::to<std::vector>();
|
|
337
|
+
std::ranges::sort(keys);
|
|
338
|
+
|
|
339
|
+
uint64_t start = iterator;
|
|
340
|
+
uint64_t available = keys.size() > start ? keys.size() - start : 0;
|
|
341
|
+
uint64_t to_take = std::min<uint64_t>(available, per_iteration);
|
|
342
|
+
|
|
343
|
+
resp.result.reserve(to_take);
|
|
344
|
+
|
|
345
|
+
auto i = iterator;
|
|
346
|
+
while (i < iterator + per_iteration && i < keys.size()) {
|
|
347
|
+
const auto &key = keys[i];
|
|
348
|
+
resp.result.emplace_back(key, get(key, attributes));
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
resp.iterator_version = this->iterator_version;
|
|
352
|
+
resp.iterator = i < keys.size() ? i : 0;
|
|
353
|
+
return glz::write_json(resp).value_or("error"); // TODO don't use json
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
void begin_transaction(uint64_t estimated_number_of_items = 0) {
|
|
358
|
+
if (transactionOpened) {
|
|
359
|
+
throw std::runtime_error("Transaction already open");
|
|
360
|
+
}
|
|
361
|
+
if (estimated_number_of_items != 0) {
|
|
362
|
+
cache.reserve(estimated_number_of_items);
|
|
363
|
+
increaseIteratorVersion();
|
|
364
|
+
}
|
|
365
|
+
oldCacheSize = cache.size();
|
|
366
|
+
transactionOpened = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
void end_transaction() {
|
|
370
|
+
if (!transactionOpened) {
|
|
371
|
+
throw std::runtime_error("Transaction not opened");
|
|
372
|
+
}
|
|
373
|
+
for (auto it = cache.begin(); it != cache.end();) {
|
|
374
|
+
if (it->second.isNew) {
|
|
375
|
+
it.value().isNew = false;
|
|
376
|
+
++it;
|
|
377
|
+
} else {
|
|
378
|
+
it = cache.erase(it);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
increaseIteratorVersion();
|
|
382
|
+
transactionOpened = false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
size_t load_page(const str &json_text) {
|
|
386
|
+
if (!transactionOpened) {
|
|
387
|
+
throw std::runtime_error("Transaction not opened");
|
|
388
|
+
}
|
|
389
|
+
json::Response resp;
|
|
390
|
+
if (auto ce = glz::read<glz::opts{.error_on_unknown_keys = false}>(resp, json_text))
|
|
391
|
+
throw std::runtime_error(glz::format_error(ce, json_text));
|
|
392
|
+
cache.reserve(resp.result.count);
|
|
393
|
+
for (auto &item: resp.result.data) {
|
|
394
|
+
auto &marked_item = cache[item.id];
|
|
395
|
+
std::unordered_map<str, pyAttrValue> attrs;
|
|
396
|
+
for (auto &attr: item.attributes)
|
|
397
|
+
attrs[attr.id] = convert_valueJ(attr.value);
|
|
398
|
+
|
|
399
|
+
setMarkedItem(marked_item, attrs);
|
|
400
|
+
}
|
|
401
|
+
increaseIteratorVersion();
|
|
402
|
+
return resp.result.pagination.pages;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
void increaseIteratorVersion() {
|
|
406
|
+
iterator_version++;
|
|
407
|
+
iterator_version++;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
tsl::sparse_map<str, MarkedItem> cache;
|
|
411
|
+
absl::flat_hash_map<str, uint8_t, absl::Hash<str> > attrMap;
|
|
412
|
+
strVec attrIdx;
|
|
413
|
+
const uint8_t numberOfAttributes;
|
|
414
|
+
size_t oldCacheSize = 0;
|
|
415
|
+
bool transactionOpened = false;
|
|
416
|
+
uint64_t iterator_version = 1; // for get_all()
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
NB_MODULE(_small_cache_impl, m) {
|
|
421
|
+
nb::class_<SmallCache> cache(m, "SmallCache");
|
|
422
|
+
cache
|
|
423
|
+
.def(nb::init<std::vector<std::string> >(), nb::arg("attribute_names"))
|
|
424
|
+
.def("begin_transaction", &SmallCache::begin_transaction, nb::arg("estimated_number_of_items") = 0)
|
|
425
|
+
.def("end_transaction", &SmallCache::end_transaction)
|
|
426
|
+
.def("add", &SmallCache::add_item, nb::arg("item_id"), nb::arg("attributes"))
|
|
427
|
+
.def("get", &SmallCache::get, nb::arg("id"), nb::arg("attributes"))
|
|
428
|
+
.def("get_all", &SmallCache::get_all, nb::arg("attributes"), nb::arg("per_iteration") = 10000,
|
|
429
|
+
nb::arg("iterator_version") = 0,
|
|
430
|
+
nb::arg("iterator") = 0)
|
|
431
|
+
.def("load_page", &SmallCache::load_page, nb::arg("json_text"));
|
|
432
|
+
// nb::class_<SmallCache::getAllResponse>(cache, "getAllResponse")
|
|
433
|
+
// .def(nb::init<>())
|
|
434
|
+
// .def_rw("iterator_version", &SmallCache::getAllResponse::iterator_version)
|
|
435
|
+
// .def_rw("iterator", &SmallCache::getAllResponse::iterator)
|
|
436
|
+
// .def_rw("result", &SmallCache::getAllResponse::result);
|
|
437
|
+
}
|