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.
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ # Maintain dependencies for GitHub Actions
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ actions:
10
+ patterns:
11
+ - "*"
@@ -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,9 @@
1
+ nanobind_test.egg-info
2
+ _skbuild
3
+ /dist
4
+ __pycache__
5
+ build
6
+ *.pyd
7
+ *.egg-info
8
+ .vscode
9
+ .vs
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import small_cache as m
2
+
3
+ def test_create():
4
+ m.SmallCache(["test1","2"])
5
+