hgraph-analytics 0.8.5__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.
- hgraph_analytics-0.8.5/.gitignore +7 -0
- hgraph_analytics-0.8.5/CHANGELOG.md +8 -0
- hgraph_analytics-0.8.5/CMakeLists.txt +117 -0
- hgraph_analytics-0.8.5/LICENSE +21 -0
- hgraph_analytics-0.8.5/PKG-INFO +45 -0
- hgraph_analytics-0.8.5/README.md +32 -0
- hgraph_analytics-0.8.5/cmake/hgraph-analyticsConfig.cmake.in +8 -0
- hgraph_analytics-0.8.5/include/hgraph/analytics/export.h +16 -0
- hgraph_analytics-0.8.5/include/hgraph/analytics/operators.h +124 -0
- hgraph_analytics-0.8.5/pyproject.toml +47 -0
- hgraph_analytics-0.8.5/python/hgraph_analytics/__init__.py +88 -0
- hgraph_analytics-0.8.5/python/hgraph_analytics/py.typed +1 -0
- hgraph_analytics-0.8.5/python/tests/test_distribution_audit.py +38 -0
- hgraph_analytics-0.8.5/python/tests/test_pct_change.py +106 -0
- hgraph_analytics-0.8.5/src/operators.cpp +148 -0
- hgraph_analytics-0.8.5/src/python_module.cpp +11 -0
- hgraph_analytics-0.8.5/test_package/CMakeLists.txt +24 -0
- hgraph_analytics-0.8.5/test_package/main.cpp +40 -0
- hgraph_analytics-0.8.5/tests/CMakeLists.txt +17 -0
- hgraph_analytics-0.8.5/tests/test_pct_change.cpp +256 -0
- hgraph_analytics-0.8.5/tools/audit_distribution.py +121 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- Move `diff`, `count`, `clip`, `ewma`, `center_of_mass_to_alpha`,
|
|
6
|
+
`span_to_alpha`, and `pct_change` from core into `hgraph-analytics`.
|
|
7
|
+
- Extend the C++-first `pct_change` graph with observation-count periods and an
|
|
8
|
+
explicit divide-by-zero policy.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.25)
|
|
2
|
+
|
|
3
|
+
project(hgraph_analytics VERSION 0.8.0 LANGUAGES CXX)
|
|
4
|
+
|
|
5
|
+
include(GNUInstallDirs)
|
|
6
|
+
include(CMakePackageConfigHelpers)
|
|
7
|
+
include(CTest)
|
|
8
|
+
|
|
9
|
+
option(HGRAPH_ANALYTICS_BUILD_PYTHON "Build the optional Python authoring bridge" OFF)
|
|
10
|
+
option(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS
|
|
11
|
+
"Treat hgraph-analytics compiler warnings as errors"
|
|
12
|
+
${HGRAPH_WARNINGS_AS_ERRORS})
|
|
13
|
+
|
|
14
|
+
if(HGRAPH_ANALYTICS_BUILD_PYTHON)
|
|
15
|
+
find_package(Python 3.12 COMPONENTS
|
|
16
|
+
Interpreter Development.Module Development.SABIModule REQUIRED)
|
|
17
|
+
endif()
|
|
18
|
+
|
|
19
|
+
if(NOT TARGET hgraph::core)
|
|
20
|
+
find_package(hgraph CONFIG REQUIRED)
|
|
21
|
+
endif()
|
|
22
|
+
|
|
23
|
+
if(HGRAPH_ANALYTICS_BUILD_PYTHON)
|
|
24
|
+
get_target_property(_hgraph_analytics_core_links hgraph::options INTERFACE_LINK_LIBRARIES)
|
|
25
|
+
if("Python::Python" IN_LIST _hgraph_analytics_core_links)
|
|
26
|
+
message(FATAL_ERROR
|
|
27
|
+
"The selected hgraph SDK embeds a specific Python interpreter and "
|
|
28
|
+
"cannot produce an ABI3 hgraph-analytics wheel; use the SDK installed "
|
|
29
|
+
"by an hgraph stable-ABI wheel")
|
|
30
|
+
endif()
|
|
31
|
+
endif()
|
|
32
|
+
|
|
33
|
+
add_library(hgraph_analytics
|
|
34
|
+
src/operators.cpp
|
|
35
|
+
)
|
|
36
|
+
add_library(hgraph::analytics ALIAS hgraph_analytics)
|
|
37
|
+
|
|
38
|
+
get_target_property(HGRAPH_ANALYTICS_LIBRARY_TYPE hgraph_analytics TYPE)
|
|
39
|
+
if(HGRAPH_ANALYTICS_LIBRARY_TYPE STREQUAL "STATIC_LIBRARY")
|
|
40
|
+
target_compile_definitions(hgraph_analytics PUBLIC HGRAPH_ANALYTICS_STATIC_DEFINE)
|
|
41
|
+
endif()
|
|
42
|
+
|
|
43
|
+
target_compile_features(hgraph_analytics PUBLIC cxx_std_23)
|
|
44
|
+
target_link_libraries(hgraph_analytics PUBLIC hgraph::core)
|
|
45
|
+
target_include_directories(hgraph_analytics
|
|
46
|
+
PUBLIC
|
|
47
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
48
|
+
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
|
49
|
+
)
|
|
50
|
+
set_target_properties(hgraph_analytics PROPERTIES
|
|
51
|
+
EXPORT_NAME analytics
|
|
52
|
+
POSITION_INDEPENDENT_CODE ON
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if(MSVC)
|
|
56
|
+
target_compile_options(hgraph_analytics PRIVATE /W4 /permissive-)
|
|
57
|
+
if(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS)
|
|
58
|
+
target_compile_options(hgraph_analytics PRIVATE /WX)
|
|
59
|
+
endif()
|
|
60
|
+
else()
|
|
61
|
+
target_compile_options(hgraph_analytics PRIVATE -Wall -Wextra -Wpedantic)
|
|
62
|
+
if(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS)
|
|
63
|
+
target_compile_options(hgraph_analytics PRIVATE -Werror)
|
|
64
|
+
endif()
|
|
65
|
+
endif()
|
|
66
|
+
|
|
67
|
+
if(BUILD_TESTING)
|
|
68
|
+
add_subdirectory(tests)
|
|
69
|
+
endif()
|
|
70
|
+
|
|
71
|
+
if(HGRAPH_ANALYTICS_BUILD_PYTHON)
|
|
72
|
+
if(NOT COMMAND hgraph_add_python_module OR NOT TARGET hgraph::nanobind)
|
|
73
|
+
message(FATAL_ERROR
|
|
74
|
+
"HGRAPH_ANALYTICS_BUILD_PYTHON requires a Python-enabled installed hgraph SDK")
|
|
75
|
+
endif()
|
|
76
|
+
hgraph_add_python_module(_hgraph_analytics STABLE_ABI NOMINSIZE src/python_module.cpp)
|
|
77
|
+
target_link_libraries(_hgraph_analytics PRIVATE hgraph::analytics)
|
|
78
|
+
install(TARGETS _hgraph_analytics
|
|
79
|
+
COMPONENT Python
|
|
80
|
+
LIBRARY DESTINATION hgraph_analytics
|
|
81
|
+
RUNTIME DESTINATION hgraph_analytics
|
|
82
|
+
)
|
|
83
|
+
endif()
|
|
84
|
+
|
|
85
|
+
install(TARGETS hgraph_analytics
|
|
86
|
+
EXPORT hgraphAnalyticsTargets
|
|
87
|
+
COMPONENT Development
|
|
88
|
+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
89
|
+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
|
90
|
+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
|
91
|
+
)
|
|
92
|
+
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
|
93
|
+
COMPONENT Development FILES_MATCHING PATTERN "*.h")
|
|
94
|
+
|
|
95
|
+
write_basic_package_version_file(
|
|
96
|
+
"${PROJECT_BINARY_DIR}/hgraph-analyticsConfigVersion.cmake"
|
|
97
|
+
VERSION ${PROJECT_VERSION}
|
|
98
|
+
COMPATIBILITY SameMajorVersion
|
|
99
|
+
)
|
|
100
|
+
configure_package_config_file(
|
|
101
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/hgraph-analyticsConfig.cmake.in"
|
|
102
|
+
"${PROJECT_BINARY_DIR}/hgraph-analyticsConfig.cmake"
|
|
103
|
+
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
install(EXPORT hgraphAnalyticsTargets
|
|
107
|
+
FILE hgraphAnalyticsTargets.cmake
|
|
108
|
+
NAMESPACE hgraph::
|
|
109
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
|
|
110
|
+
COMPONENT Development
|
|
111
|
+
)
|
|
112
|
+
install(FILES
|
|
113
|
+
"${PROJECT_BINARY_DIR}/hgraph-analyticsConfig.cmake"
|
|
114
|
+
"${PROJECT_BINARY_DIR}/hgraph-analyticsConfigVersion.cmake"
|
|
115
|
+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
|
|
116
|
+
COMPONENT Development
|
|
117
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Howard Henson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: hgraph-analytics
|
|
3
|
+
Version: 0.8.5
|
|
4
|
+
Summary: C++-first numerical analytics for hgraph
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/hhenson/hgraph
|
|
7
|
+
Project-URL: Repository, https://github.com/hhenson/hgraph.git
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: hgraph>=0.8.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# hgraph-analytics
|
|
15
|
+
|
|
16
|
+
C++-first numerical analytics for hgraph. The package owns the numerical
|
|
17
|
+
analytical family migrated from core—`diff`, `count`, `clip`, `ewma`, and
|
|
18
|
+
`pct_change`—plus the EWMA parameter conversion helpers.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import hgraph as hg
|
|
22
|
+
import hgraph_analytics as hga
|
|
23
|
+
|
|
24
|
+
change = hga.pct_change(
|
|
25
|
+
value,
|
|
26
|
+
period=12,
|
|
27
|
+
divide_by_zero=hg.DivideByZero.NAN,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
bounded = hga.clip(change, -0.25, 0.25)
|
|
31
|
+
smoothed = hga.ewma(bounded, alpha=0.2)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The result is fractional: `0.05` denotes five percent. `period` counts valid
|
|
35
|
+
source observations and must be positive. The operator does not infer
|
|
36
|
+
dataframe ordering, elapsed-time sampling, market sessions, or financial price
|
|
37
|
+
adjustment.
|
|
38
|
+
|
|
39
|
+
Native consumers link `hgraph::analytics`, call
|
|
40
|
+
`hgraph::analytics::register_analytics_operators()`, and wire
|
|
41
|
+
the markers in `hgraph::analytics`, including `diff`, `count`, `clip`, `ewma`,
|
|
42
|
+
and `pct_change`.
|
|
43
|
+
|
|
44
|
+
See the hgraph user-guide migration note for the complete Python and C++ name
|
|
45
|
+
mapping from the former core API.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# hgraph-analytics
|
|
2
|
+
|
|
3
|
+
C++-first numerical analytics for hgraph. The package owns the numerical
|
|
4
|
+
analytical family migrated from core—`diff`, `count`, `clip`, `ewma`, and
|
|
5
|
+
`pct_change`—plus the EWMA parameter conversion helpers.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import hgraph as hg
|
|
9
|
+
import hgraph_analytics as hga
|
|
10
|
+
|
|
11
|
+
change = hga.pct_change(
|
|
12
|
+
value,
|
|
13
|
+
period=12,
|
|
14
|
+
divide_by_zero=hg.DivideByZero.NAN,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
bounded = hga.clip(change, -0.25, 0.25)
|
|
18
|
+
smoothed = hga.ewma(bounded, alpha=0.2)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The result is fractional: `0.05` denotes five percent. `period` counts valid
|
|
22
|
+
source observations and must be positive. The operator does not infer
|
|
23
|
+
dataframe ordering, elapsed-time sampling, market sessions, or financial price
|
|
24
|
+
adjustment.
|
|
25
|
+
|
|
26
|
+
Native consumers link `hgraph::analytics`, call
|
|
27
|
+
`hgraph::analytics::register_analytics_operators()`, and wire
|
|
28
|
+
the markers in `hgraph::analytics`, including `diff`, `count`, `clip`, `ewma`,
|
|
29
|
+
and `pct_change`.
|
|
30
|
+
|
|
31
|
+
See the hgraph user-guide migration note for the complete Python and C++ name
|
|
32
|
+
mapping from the former core API.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#ifndef HGRAPH_ANALYTICS_EXPORT_H
|
|
2
|
+
#define HGRAPH_ANALYTICS_EXPORT_H
|
|
3
|
+
|
|
4
|
+
#if defined(HGRAPH_ANALYTICS_STATIC_DEFINE)
|
|
5
|
+
#define HGRAPH_ANALYTICS_EXPORT
|
|
6
|
+
#elif defined(_WIN32)
|
|
7
|
+
#if defined(hgraph_analytics_EXPORTS)
|
|
8
|
+
#define HGRAPH_ANALYTICS_EXPORT __declspec(dllexport)
|
|
9
|
+
#else
|
|
10
|
+
#define HGRAPH_ANALYTICS_EXPORT __declspec(dllimport)
|
|
11
|
+
#endif
|
|
12
|
+
#else
|
|
13
|
+
#define HGRAPH_ANALYTICS_EXPORT __attribute__((visibility("default")))
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
#endif // HGRAPH_ANALYTICS_EXPORT_H
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#ifndef HGRAPH_ANALYTICS_OPERATORS_H
|
|
2
|
+
#define HGRAPH_ANALYTICS_OPERATORS_H
|
|
3
|
+
|
|
4
|
+
#include <hgraph/analytics/export.h>
|
|
5
|
+
|
|
6
|
+
#include <hgraph/lib/std/operators/arithmetic.h>
|
|
7
|
+
#include <hgraph/types/operator_dispatch.h>
|
|
8
|
+
#include <hgraph/types/primitive_types.h>
|
|
9
|
+
#include <hgraph/types/static_schema.h>
|
|
10
|
+
|
|
11
|
+
namespace hgraph::analytics
|
|
12
|
+
{
|
|
13
|
+
/** Subtract the preceding valid observation from the current observation.
|
|
14
|
+
The first valid observation warms the retained prior-value state and does
|
|
15
|
+
not produce output. Invalid cycles neither update that state nor trigger.
|
|
16
|
+
@param ts Integer or floating-point input stream.
|
|
17
|
+
@return Successive differences with the same scalar type as ``ts``.
|
|
18
|
+
@par Python example
|
|
19
|
+
@code{.py}
|
|
20
|
+
import hgraph_analytics as hga
|
|
21
|
+
change = hga.diff(price)
|
|
22
|
+
@endcode */
|
|
23
|
+
struct diff
|
|
24
|
+
: Operator<"hgraph.analytics.diff", In<"ts", TS<ScalarVar<"T">>>,
|
|
25
|
+
Out<TS<ScalarVar<"T">>>>
|
|
26
|
+
{
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Count valid input ticks cumulatively, with an optional reset signal.
|
|
30
|
+
A same-cycle reset is applied before a source tick is admitted. A reset-only
|
|
31
|
+
cycle clears state without producing output.
|
|
32
|
+
@param ts Signal or stream whose valid ticks are counted; values are ignored.
|
|
33
|
+
@param reset Optional signal that restarts the count from one on the next
|
|
34
|
+
same-cycle or subsequent source tick.
|
|
35
|
+
@return Running integer tick count.
|
|
36
|
+
@par Python example
|
|
37
|
+
@code{.py}
|
|
38
|
+
import hgraph_analytics as hga
|
|
39
|
+
session_count = hga.count(updates, reset=session_start)
|
|
40
|
+
@endcode */
|
|
41
|
+
struct count
|
|
42
|
+
: Operator<"hgraph.analytics.count", In<"ts", SIGNAL>, Out<TS<Int>>>
|
|
43
|
+
{
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Constrain each numeric observation to an inclusive range.
|
|
47
|
+
Bounds are fixed at wiring time and must share the resolved integer or
|
|
48
|
+
floating-point type of the input. The node has no warm-up state and emits
|
|
49
|
+
for every valid source tick.
|
|
50
|
+
@param ts Numeric input stream.
|
|
51
|
+
@param min Lower inclusive bound.
|
|
52
|
+
@param max Upper inclusive bound.
|
|
53
|
+
@return ``min`` below the range, ``max`` above it, otherwise ``ts``.
|
|
54
|
+
@throws std::invalid_argument during node start when ``min > max``.
|
|
55
|
+
@par Python example
|
|
56
|
+
@code{.py}
|
|
57
|
+
import hgraph_analytics as hga
|
|
58
|
+
bounded = hga.clip(ratio, 0.0, 1.0)
|
|
59
|
+
@endcode */
|
|
60
|
+
struct clip
|
|
61
|
+
: Operator<"hgraph.analytics.clip", In<"ts", TS<ScalarVar<"T">>>,
|
|
62
|
+
Scalar<"min", ScalarVar<"T">>, Scalar<"max", ScalarVar<"T">>,
|
|
63
|
+
Out<TS<ScalarVar<"T">>>>
|
|
64
|
+
{
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Compute an exponentially weighted moving average of floating observations.
|
|
68
|
+
The first valid observation initializes the retained average. Each later
|
|
69
|
+
valid tick emits ``alpha * current + (1 - alpha) * previous``. Invalid
|
|
70
|
+
cycles neither update state nor trigger output.
|
|
71
|
+
@param ts Floating-point input stream.
|
|
72
|
+
@param alpha Wiring-time smoothing factor. Values are applied as supplied
|
|
73
|
+
for compatibility with the migrated 0.8 operator.
|
|
74
|
+
@return Running exponentially weighted moving average.
|
|
75
|
+
@par Python example
|
|
76
|
+
@code{.py}
|
|
77
|
+
import hgraph_analytics as hga
|
|
78
|
+
smoothed = hga.ewma(price, alpha=0.2)
|
|
79
|
+
@endcode */
|
|
80
|
+
struct ewma
|
|
81
|
+
: Operator<"hgraph.analytics.ewma", In<"ts", TS<Float>>,
|
|
82
|
+
Scalar<"alpha", Float>, Out<TS<Float>>>
|
|
83
|
+
{
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Compute fractional change from an earlier valid observation.
|
|
87
|
+
For a positive observation-count ``period``, the result is
|
|
88
|
+
``(current - prior) / prior``. The first ``period`` valid observations
|
|
89
|
+
produce no output. Invalid source cycles are not counted, and the graph
|
|
90
|
+
ticks only when a valid source observation arrives.
|
|
91
|
+
@param ts Integer or floating-point input stream.
|
|
92
|
+
@param period Positive valid-observation count fixed at wiring time;
|
|
93
|
+
defaults to one. Durations and negative look-ahead periods
|
|
94
|
+
are not supported.
|
|
95
|
+
@param divide_by_zero Wiring-time policy applied when the prior value is
|
|
96
|
+
zero; defaults to ``DivideByZero::Error``.
|
|
97
|
+
@return Floating-point fractional change. ``0.05`` denotes five percent.
|
|
98
|
+
@throws std::invalid_argument while wiring when ``period`` is not positive.
|
|
99
|
+
@throws std::domain_error during evaluation for a zero prior value when
|
|
100
|
+
``divide_by_zero`` is ``Error``.
|
|
101
|
+
@par Python example
|
|
102
|
+
@code{.py}
|
|
103
|
+
import hgraph as hg
|
|
104
|
+
import hgraph_analytics as hga
|
|
105
|
+
|
|
106
|
+
change = hga.pct_change(price, period=12,
|
|
107
|
+
divide_by_zero=hg.DivideByZero.NAN)
|
|
108
|
+
@endcode */
|
|
109
|
+
struct pct_change
|
|
110
|
+
: Operator<"hgraph.analytics.pct_change",
|
|
111
|
+
In<"ts", TS<ScalarVar<"T">>>,
|
|
112
|
+
Scalar<"period", Int>,
|
|
113
|
+
Scalar<"divide_by_zero", stdlib::DivideByZero>,
|
|
114
|
+
Out<TS<Float>>>
|
|
115
|
+
{
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Register the hgraph-analytics overloads in the current hgraph registry.
|
|
119
|
+
Call once per registry lifetime after core standard operators are
|
|
120
|
+
available and before wiring analytics graphs. */
|
|
121
|
+
HGRAPH_ANALYTICS_EXPORT void register_analytics_operators();
|
|
122
|
+
} // namespace hgraph::analytics
|
|
123
|
+
|
|
124
|
+
#endif // HGRAPH_ANALYTICS_OPERATORS_H
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"scikit-build-core>=0.11",
|
|
4
|
+
"nanobind==2.13.0",
|
|
5
|
+
"pyarrow>=25,<26",
|
|
6
|
+
"hgraph>=0.8.0",
|
|
7
|
+
]
|
|
8
|
+
build-backend = "scikit_build_core.build"
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "hgraph-analytics"
|
|
12
|
+
version = "0.8.5"
|
|
13
|
+
description = "C++-first numerical analytics for hgraph"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
license = {text = "MIT"}
|
|
16
|
+
requires-python = ">=3.12"
|
|
17
|
+
dependencies = ["hgraph>=0.8.0"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/hhenson/hgraph"
|
|
21
|
+
Repository = "https://github.com/hhenson/hgraph.git"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = ["pytest>=8"]
|
|
25
|
+
|
|
26
|
+
[tool.scikit-build]
|
|
27
|
+
cmake.source-dir = "."
|
|
28
|
+
cmake.args = [
|
|
29
|
+
"-DHGRAPH_ANALYTICS_BUILD_PYTHON=ON",
|
|
30
|
+
"-DBUILD_TESTING=OFF",
|
|
31
|
+
]
|
|
32
|
+
install.components = ["Python", "Development"]
|
|
33
|
+
wheel.packages = ["python/hgraph_analytics"]
|
|
34
|
+
wheel.py-api = "cp312"
|
|
35
|
+
sdist.include = [
|
|
36
|
+
"CMakeLists.txt",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md",
|
|
40
|
+
"cmake/**",
|
|
41
|
+
"include/**",
|
|
42
|
+
"python/**",
|
|
43
|
+
"src/**",
|
|
44
|
+
"tests/**",
|
|
45
|
+
"test_package/**",
|
|
46
|
+
"tools/**",
|
|
47
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""C++-first numerical analytics for hgraph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from hgraph import DivideByZero, NUMBER, SIGNAL, TS, operator_function
|
|
6
|
+
|
|
7
|
+
from . import _hgraph_analytics as _native
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_diff = operator_function("hgraph.analytics.diff")
|
|
11
|
+
_count = operator_function("hgraph.analytics.count")
|
|
12
|
+
_clip = operator_function("hgraph.analytics.clip")
|
|
13
|
+
_ewma = operator_function("hgraph.analytics.ewma")
|
|
14
|
+
_pct_change = operator_function("hgraph.analytics.pct_change")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def diff(ts: TS[NUMBER]) -> TS[NUMBER]:
|
|
18
|
+
"""Return the difference from the preceding valid observation."""
|
|
19
|
+
|
|
20
|
+
return _diff(ts)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def count(ts: SIGNAL, reset: SIGNAL = None) -> TS[int]:
|
|
24
|
+
"""Count valid input ticks, restarting when ``reset`` ticks."""
|
|
25
|
+
|
|
26
|
+
return _count(ts) if reset is None else _count(ts, reset)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def clip(ts: TS[NUMBER], min_: NUMBER, max_: NUMBER) -> TS[NUMBER]:
|
|
30
|
+
"""Constrain each input value to the inclusive ``[min_, max_]`` range."""
|
|
31
|
+
|
|
32
|
+
return _clip(ts, min_, max_)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ewma(ts: TS[float], alpha: float) -> TS[float]:
|
|
36
|
+
"""Return the exponentially weighted moving average of ``ts``."""
|
|
37
|
+
|
|
38
|
+
return _ewma(ts, alpha)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def center_of_mass_to_alpha(com: float) -> float:
|
|
42
|
+
"""Convert a positive center of mass to an EWMA smoothing factor."""
|
|
43
|
+
|
|
44
|
+
if com <= 0:
|
|
45
|
+
raise ValueError(f"Center of mass must be positive, got {com}")
|
|
46
|
+
return 1.0 / (com + 1.0)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def span_to_alpha(span: float) -> float:
|
|
50
|
+
"""Convert a positive span to an EWMA smoothing factor."""
|
|
51
|
+
|
|
52
|
+
if span <= 0:
|
|
53
|
+
raise ValueError(f"Span must be positive, got {span}")
|
|
54
|
+
return 2.0 / (span + 1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pct_change(
|
|
58
|
+
ts: TS[NUMBER],
|
|
59
|
+
period: int = 1,
|
|
60
|
+
divide_by_zero: DivideByZero = DivideByZero.ERROR,
|
|
61
|
+
) -> TS[float]:
|
|
62
|
+
"""Return fractional change from ``period`` valid observations earlier.
|
|
63
|
+
|
|
64
|
+
``0.05`` denotes five percent. The first ``period`` observations do not
|
|
65
|
+
produce output, invalid input cycles are not counted, and output triggers
|
|
66
|
+
only on a valid input tick. ``period`` is a positive wiring-time observation
|
|
67
|
+
count and defaults to one. ``divide_by_zero`` is a wiring-time policy for a
|
|
68
|
+
zero prior value and defaults to ``DivideByZero.ERROR``. The retained state
|
|
69
|
+
is the core lag history for ``period`` observations.
|
|
70
|
+
|
|
71
|
+
A non-positive period raises ``WiringError`` while wiring. A zero prior
|
|
72
|
+
raises during evaluation under the default division policy. This operator
|
|
73
|
+
does not infer elapsed-time, dataframe-row, sampling, or financial-return
|
|
74
|
+
semantics.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return _pct_change(ts, period, divide_by_zero)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"center_of_mass_to_alpha",
|
|
82
|
+
"clip",
|
|
83
|
+
"count",
|
|
84
|
+
"diff",
|
|
85
|
+
"ewma",
|
|
86
|
+
"pct_change",
|
|
87
|
+
"span_to_alpha",
|
|
88
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
ANALYTICS_ROOT = Path(__file__).resolve().parents[2]
|
|
11
|
+
AUDIT_SCRIPT = ANALYTICS_ROOT / "tools/audit_distribution.py"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("library_directory", ["lib", "lib64"])
|
|
15
|
+
def test_wheel_audit_accepts_platform_library_directories(
|
|
16
|
+
tmp_path: Path, library_directory: str
|
|
17
|
+
):
|
|
18
|
+
wheel = tmp_path / "hgraph_analytics-0.0.0-cp312-abi3-any.whl"
|
|
19
|
+
files = {
|
|
20
|
+
"hgraph_analytics/__init__.py": "",
|
|
21
|
+
"hgraph_analytics/py.typed": "",
|
|
22
|
+
"hgraph_analytics/_hgraph_analytics.abi3.so": "",
|
|
23
|
+
"include/hgraph/analytics/operators.h": "",
|
|
24
|
+
f"{library_directory}/libhgraph_analytics.a": "",
|
|
25
|
+
f"{library_directory}/cmake/hgraph-analytics/hgraph-analyticsConfig.cmake": "",
|
|
26
|
+
f"{library_directory}/cmake/hgraph-analytics/hgraphAnalyticsTargets.cmake": "",
|
|
27
|
+
"hgraph_analytics-0.0.0.dist-info/METADATA": (
|
|
28
|
+
"Metadata-Version: 2.2\n"
|
|
29
|
+
"Name: hgraph-analytics\n"
|
|
30
|
+
"Version: 0.0.0\n"
|
|
31
|
+
"Requires-Dist: hgraph\n"
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
with zipfile.ZipFile(wheel, "w") as archive:
|
|
35
|
+
for name, contents in files.items():
|
|
36
|
+
archive.writestr(name, contents)
|
|
37
|
+
|
|
38
|
+
subprocess.run([sys.executable, AUDIT_SCRIPT, wheel], check=True)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import hgraph as hg
|
|
6
|
+
import hgraph_analytics as hga
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_migrated_analytical_helpers():
|
|
10
|
+
assert hga.center_of_mass_to_alpha(1.0) == 0.5
|
|
11
|
+
assert hga.span_to_alpha(1.0) == 1.0
|
|
12
|
+
with pytest.raises(ValueError, match="Center of mass must be positive"):
|
|
13
|
+
hga.center_of_mass_to_alpha(0.0)
|
|
14
|
+
with pytest.raises(ValueError, match="Span must be positive"):
|
|
15
|
+
hga.span_to_alpha(0.0)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_diff_count_clip_and_ewma():
|
|
19
|
+
assert hg.eval_node(hga.diff, [1, 2, 4, 7]) == [None, 1, 2, 3]
|
|
20
|
+
assert hg.eval_node(hga.count, [3, None, 2, 1]) == [1, None, 2, 3]
|
|
21
|
+
assert hg.eval_node(
|
|
22
|
+
hga.count,
|
|
23
|
+
[3, 2, 1],
|
|
24
|
+
reset=[None, True, None],
|
|
25
|
+
resolution_dict={"ts": hg.TS[int], "reset": hg.TS[bool]},
|
|
26
|
+
) == [1, 1, 2]
|
|
27
|
+
assert hg.eval_node(
|
|
28
|
+
hga.count,
|
|
29
|
+
[3, None, 2, 1],
|
|
30
|
+
reset=[None, True, None],
|
|
31
|
+
resolution_dict={"ts": hg.TS[int], "reset": hg.TS[bool]},
|
|
32
|
+
) == [1, None, 1, 2]
|
|
33
|
+
assert hg.eval_node(hga.clip, [-1, 1, 3], 0, 2) == [0, 1, 2]
|
|
34
|
+
assert hg.eval_node(hga.clip, [-1.0, 0.5, 2.0], 0.0, 1.0) == [
|
|
35
|
+
0.0,
|
|
36
|
+
0.5,
|
|
37
|
+
1.0,
|
|
38
|
+
]
|
|
39
|
+
assert hg.eval_node(hga.ewma, [1.0, 2.0, 3.0, 4.0], 0.5) == [
|
|
40
|
+
1.0,
|
|
41
|
+
1.5,
|
|
42
|
+
2.25,
|
|
43
|
+
3.125,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_count_accepts_a_mapped_dictionary_signal():
|
|
48
|
+
@hg.graph
|
|
49
|
+
def app(tsd: hg.TSD[int, hg.TS[int]]) -> hg.TS[int]:
|
|
50
|
+
return hga.count(hg.map_(lambda value: value + 1, tsd))
|
|
51
|
+
|
|
52
|
+
assert hg.eval_node(
|
|
53
|
+
app,
|
|
54
|
+
[{1: 10}, {2: 20}, None, {1: hg.REMOVE}],
|
|
55
|
+
) == [1, 2, None, 3]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_clip_rejects_reversed_bounds():
|
|
59
|
+
with pytest.raises(Exception, match="min must be <= max"):
|
|
60
|
+
hg.eval_node(hga.clip, [1.0], 1.0, -1.0)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_default_and_sparse_observations():
|
|
64
|
+
assert hg.eval_node(hga.pct_change, [1, 2, 3]) == [None, 1.0, 0.5]
|
|
65
|
+
assert hg.eval_node(hga.pct_change, [1, None, 2]) == [None, None, 1.0]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_longer_period_and_float_input():
|
|
69
|
+
result = hg.eval_node(hga.pct_change, [10.0, 11.0, 12.0, 15.0], 2)
|
|
70
|
+
assert result[:2] == [None, None]
|
|
71
|
+
assert result[2:] == pytest.approx([0.2, 4.0 / 11.0])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize(
|
|
75
|
+
("policy", "expected"),
|
|
76
|
+
[
|
|
77
|
+
(hg.DivideByZero.NAN, math.nan),
|
|
78
|
+
(hg.DivideByZero.ZERO, 0.0),
|
|
79
|
+
(hg.DivideByZero.ONE, 1.0),
|
|
80
|
+
(hg.DivideByZero.NONE, None),
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
def test_zero_denominator_policies(policy, expected):
|
|
84
|
+
result = hg.eval_node(hga.pct_change, [0.0, 1.0, 2.0], 1, policy)
|
|
85
|
+
assert result[0] is None
|
|
86
|
+
if math.isnan(expected) if isinstance(expected, float) else False:
|
|
87
|
+
assert math.isnan(result[1])
|
|
88
|
+
else:
|
|
89
|
+
assert result[1] == expected
|
|
90
|
+
assert result[2] == 1.0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_zero_denominator_error_is_the_default():
|
|
94
|
+
with pytest.raises(Exception, match="division by zero"):
|
|
95
|
+
hg.eval_node(hga.pct_change, [0.0, 1.0])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize("period", [0, -1])
|
|
99
|
+
def test_period_must_be_positive(period):
|
|
100
|
+
with pytest.raises(hg.WiringError, match="period must be positive"):
|
|
101
|
+
hg.eval_node(hga.pct_change, [1.0, 2.0], period)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_public_signature_and_fractional_units():
|
|
105
|
+
assert hga.pct_change.__name__ == "pct_change"
|
|
106
|
+
assert hg.eval_node(hga.pct_change, [100.0, 105.0]) == [None, 0.05]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#include <hgraph/analytics/operators.h>
|
|
2
|
+
|
|
3
|
+
#include <hgraph/lib/std/operators/arithmetic.h>
|
|
4
|
+
#include <hgraph/lib/std/operators/stream.h>
|
|
5
|
+
#include <hgraph/types/graph_wiring.h>
|
|
6
|
+
#include <hgraph/types/value/value.h>
|
|
7
|
+
|
|
8
|
+
#include <algorithm>
|
|
9
|
+
#include <stdexcept>
|
|
10
|
+
#include <string_view>
|
|
11
|
+
#include <type_traits>
|
|
12
|
+
#include <utility>
|
|
13
|
+
#include <vector>
|
|
14
|
+
|
|
15
|
+
namespace hgraph::analytics
|
|
16
|
+
{
|
|
17
|
+
namespace
|
|
18
|
+
{
|
|
19
|
+
template <typename T>
|
|
20
|
+
struct diff_impl
|
|
21
|
+
{
|
|
22
|
+
static void eval(In<"ts", TS<T>> ts, RecordableState<TS<T>> prior,
|
|
23
|
+
Out<TS<T>> out)
|
|
24
|
+
{
|
|
25
|
+
// Adjacent difference is O(1) per tick and retains only the
|
|
26
|
+
// preceding valid observation. State is advanced after output
|
|
27
|
+
// so both operands belong to consecutive accepted observations.
|
|
28
|
+
if (prior.valid()) { out.set(ts.value() - prior.value().template checked_as<T>()); }
|
|
29
|
+
prior.set(ts.value());
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
struct count_impl
|
|
34
|
+
{
|
|
35
|
+
static void eval(In<"ts", SIGNAL> ts, State<Int> running_count,
|
|
36
|
+
Out<TS<Int>> out)
|
|
37
|
+
{
|
|
38
|
+
static_cast<void>(ts);
|
|
39
|
+
const Int next = running_count.get() + 1;
|
|
40
|
+
running_count.set(next);
|
|
41
|
+
out.set(next);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
struct count_reset_impl
|
|
46
|
+
{
|
|
47
|
+
static void eval(In<"ts", SIGNAL, InputValidity::Unchecked> ts,
|
|
48
|
+
In<"reset", SIGNAL, InputValidity::Unchecked> reset,
|
|
49
|
+
State<Int> running_count, Out<TS<Int>> out)
|
|
50
|
+
{
|
|
51
|
+
// Reset wins on a shared cycle, then that cycle's source tick
|
|
52
|
+
// becomes observation one. A reset by itself intentionally does
|
|
53
|
+
// not tick the output.
|
|
54
|
+
if (reset.modified()) { running_count.set(Int{0}); }
|
|
55
|
+
if (!ts.modified()) { return; }
|
|
56
|
+
const Int next = running_count.get() + 1;
|
|
57
|
+
running_count.set(next);
|
|
58
|
+
out.set(next);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
template <typename T>
|
|
63
|
+
struct clip_impl
|
|
64
|
+
{
|
|
65
|
+
static void start(Scalar<"min", T> minimum, Scalar<"max", T> maximum)
|
|
66
|
+
{
|
|
67
|
+
if (minimum.value() > maximum.value())
|
|
68
|
+
{
|
|
69
|
+
throw std::invalid_argument(
|
|
70
|
+
"hgraph.analytics.clip: min must be <= max");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static void eval(In<"ts", TS<T>> ts, Scalar<"min", T> minimum,
|
|
75
|
+
Scalar<"max", T> maximum, Out<TS<T>> out)
|
|
76
|
+
{
|
|
77
|
+
out.set(std::clamp(ts.value(), minimum.value(), maximum.value()));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
struct ewma_impl
|
|
82
|
+
{
|
|
83
|
+
static void eval(In<"ts", TS<Float>> ts, Scalar<"alpha", Float> alpha,
|
|
84
|
+
RecordableState<TS<Float>> average, Out<TS<Float>> out)
|
|
85
|
+
{
|
|
86
|
+
// This is the migrated core recurrence, kept algebraically
|
|
87
|
+
// unchanged so the package move cannot alter floating-point
|
|
88
|
+
// rounding. It is O(1) per tick and initializes from the first
|
|
89
|
+
// observation.
|
|
90
|
+
const Float value = average.valid()
|
|
91
|
+
? alpha.value() * ts.value() +
|
|
92
|
+
(Float{1.0} - alpha.value()) * average.value().checked_as<Float>()
|
|
93
|
+
: ts.value();
|
|
94
|
+
average.set(value);
|
|
95
|
+
out.set(value);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
template <typename T>
|
|
100
|
+
struct pct_change_compose
|
|
101
|
+
{
|
|
102
|
+
static constexpr auto name = std::is_same_v<T, Int>
|
|
103
|
+
? "hgraph.analytics.pct_change.int"
|
|
104
|
+
: "hgraph.analytics.pct_change.float";
|
|
105
|
+
|
|
106
|
+
static std::vector<std::pair<std::string_view, Value>> defaults()
|
|
107
|
+
{
|
|
108
|
+
return {
|
|
109
|
+
{"period", Value{Int{1}}},
|
|
110
|
+
{"divide_by_zero", Value{stdlib::DivideByZero::Error}},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static Port<TS<Float>> compose(
|
|
115
|
+
Wiring &w, NamedPort<"ts", TS<T>> ts,
|
|
116
|
+
Scalar<"period", Int> period,
|
|
117
|
+
Scalar<"divide_by_zero", stdlib::DivideByZero> divide_by_zero)
|
|
118
|
+
{
|
|
119
|
+
if (period.value() <= 0)
|
|
120
|
+
{
|
|
121
|
+
throw std::invalid_argument(
|
|
122
|
+
"hgraph.analytics.pct_change: period must be positive");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The retained observation history and readiness contract stay
|
|
126
|
+
// in core lag. Reusing the same prior port for subtraction and
|
|
127
|
+
// division guarantees both operations observe one causal anchor.
|
|
128
|
+
auto prior = wire<stdlib::lag>(w, ts, period.value());
|
|
129
|
+
auto delta = wire<stdlib::sub_>(w, ts, prior);
|
|
130
|
+
return wire<stdlib::div_, TS<Float>>(
|
|
131
|
+
w, delta, prior, divide_by_zero.value());
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
} // namespace
|
|
135
|
+
|
|
136
|
+
void register_analytics_operators()
|
|
137
|
+
{
|
|
138
|
+
register_overload<diff, diff_impl<Int>>();
|
|
139
|
+
register_overload<diff, diff_impl<Float>>();
|
|
140
|
+
register_overload<count, count_impl>();
|
|
141
|
+
register_overload<count, count_reset_impl>();
|
|
142
|
+
register_overload<clip, clip_impl<Int>>();
|
|
143
|
+
register_overload<clip, clip_impl<Float>>();
|
|
144
|
+
register_overload<ewma, ewma_impl>();
|
|
145
|
+
register_graph_overload<pct_change, pct_change_compose<Int>>();
|
|
146
|
+
register_graph_overload<pct_change, pct_change_compose<Float>>();
|
|
147
|
+
}
|
|
148
|
+
} // namespace hgraph::analytics
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include <hgraph/analytics/operators.h>
|
|
2
|
+
|
|
3
|
+
#include <nanobind/nanobind.h>
|
|
4
|
+
|
|
5
|
+
namespace nb = nanobind;
|
|
6
|
+
|
|
7
|
+
NB_MODULE(_hgraph_analytics, module)
|
|
8
|
+
{
|
|
9
|
+
hgraph::analytics::register_analytics_operators();
|
|
10
|
+
module.doc() = "Native hgraph-analytics operator registration";
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.25)
|
|
2
|
+
|
|
3
|
+
project(hgraph_analytics_consumer LANGUAGES CXX)
|
|
4
|
+
|
|
5
|
+
include(CTest)
|
|
6
|
+
|
|
7
|
+
find_package(hgraph-analytics CONFIG REQUIRED)
|
|
8
|
+
|
|
9
|
+
add_executable(hgraph_analytics_consumer main.cpp)
|
|
10
|
+
target_compile_features(hgraph_analytics_consumer PRIVATE cxx_std_23)
|
|
11
|
+
target_link_libraries(hgraph_analytics_consumer PRIVATE hgraph::analytics)
|
|
12
|
+
|
|
13
|
+
# A wheel-installed SDK exposes Python conversion hooks whose symbols are
|
|
14
|
+
# supplied by the host interpreter for extension modules. A standalone native
|
|
15
|
+
# executable using that SDK must embed Python; a Python-disabled native install
|
|
16
|
+
# exports no nanobind target and remains interpreter-free.
|
|
17
|
+
if(TARGET hgraph::nanobind AND NOT TARGET Python::Python)
|
|
18
|
+
find_package(Python 3.12 COMPONENTS Interpreter Development.Embed REQUIRED)
|
|
19
|
+
endif()
|
|
20
|
+
if(TARGET Python::Python)
|
|
21
|
+
target_link_libraries(hgraph_analytics_consumer PRIVATE Python::Python)
|
|
22
|
+
endif()
|
|
23
|
+
|
|
24
|
+
add_test(NAME hgraph_analytics_consumer COMMAND hgraph_analytics_consumer)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#include <hgraph/analytics/operators.h>
|
|
2
|
+
|
|
3
|
+
#include <hgraph/lib/std/operators/conversion.h>
|
|
4
|
+
#include <hgraph/lib/std/operators/registration.h>
|
|
5
|
+
#include <hgraph/types/graph_wiring.h>
|
|
6
|
+
|
|
7
|
+
namespace
|
|
8
|
+
{
|
|
9
|
+
namespace hg = hgraph;
|
|
10
|
+
namespace hga = hgraph::analytics;
|
|
11
|
+
|
|
12
|
+
struct InstalledConsumerGraph
|
|
13
|
+
{
|
|
14
|
+
static constexpr auto name = "installed_hgraph_analytics_consumer";
|
|
15
|
+
|
|
16
|
+
static void compose(hg::Wiring &w)
|
|
17
|
+
{
|
|
18
|
+
auto input = hg::wire<hg::stdlib::const_, hg::TS<hg::Float>>(
|
|
19
|
+
w, hg::Float{100.0});
|
|
20
|
+
auto reset = hg::wire<hg::stdlib::const_, hg::TS<hg::Bool>>(
|
|
21
|
+
w, hg::Bool{false});
|
|
22
|
+
static_cast<void>(hg::wire<hga::diff>(w, input));
|
|
23
|
+
static_cast<void>(hg::wire<hga::count>(w, input));
|
|
24
|
+
static_cast<void>(hg::wire<hga::count>(w, input, reset));
|
|
25
|
+
static_cast<void>(hg::wire<hga::clip>(
|
|
26
|
+
w, input, hg::Float{0.0}, hg::Float{200.0}));
|
|
27
|
+
static_cast<void>(hg::wire<hga::ewma>(w, input, hg::Float{0.2}));
|
|
28
|
+
static_cast<void>(hg::wire<hga::pct_change, hg::TS<hg::Float>>(
|
|
29
|
+
w, input, hg::Int{12}, hg::stdlib::DivideByZero::Nan));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
} // namespace
|
|
33
|
+
|
|
34
|
+
int main()
|
|
35
|
+
{
|
|
36
|
+
hgraph::stdlib::register_standard_operators();
|
|
37
|
+
hgraph::analytics::register_analytics_operators();
|
|
38
|
+
auto graph = hgraph::build_graph<InstalledConsumerGraph>();
|
|
39
|
+
return graph.node_count() == 0 ? 1 : 0;
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
add_executable(hgraph_analytics_tests
|
|
2
|
+
test_pct_change.cpp
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
target_compile_features(hgraph_analytics_tests PRIVATE cxx_std_23)
|
|
6
|
+
target_link_libraries(hgraph_analytics_tests PRIVATE hgraph::analytics)
|
|
7
|
+
|
|
8
|
+
if(MSVC)
|
|
9
|
+
target_compile_options(hgraph_analytics_tests PRIVATE /W4 /permissive-)
|
|
10
|
+
else()
|
|
11
|
+
target_compile_options(hgraph_analytics_tests PRIVATE -Wall -Wextra -Wpedantic)
|
|
12
|
+
endif()
|
|
13
|
+
|
|
14
|
+
add_test(NAME hgraph_analytics_tests COMMAND hgraph_analytics_tests)
|
|
15
|
+
if(COMMAND hgraph_configure_test_runtime_paths)
|
|
16
|
+
hgraph_configure_test_runtime_paths(hgraph_analytics_tests)
|
|
17
|
+
endif()
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#include <hgraph/analytics/operators.h>
|
|
2
|
+
|
|
3
|
+
#include <hgraph/lib/std/operators/registration.h>
|
|
4
|
+
#include <hgraph/lib/testing/eval_node.h>
|
|
5
|
+
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <cstddef>
|
|
8
|
+
#include <iostream>
|
|
9
|
+
#include <optional>
|
|
10
|
+
#include <stdexcept>
|
|
11
|
+
#include <string>
|
|
12
|
+
#include <type_traits>
|
|
13
|
+
#include <utility>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
namespace
|
|
17
|
+
{
|
|
18
|
+
using namespace hgraph;
|
|
19
|
+
using namespace hgraph::analytics;
|
|
20
|
+
using namespace hgraph::testing;
|
|
21
|
+
|
|
22
|
+
inline constexpr std::nullopt_t none = std::nullopt;
|
|
23
|
+
|
|
24
|
+
template <typename T, typename U>
|
|
25
|
+
[[nodiscard]] std::optional<T> optional_value(U &&value)
|
|
26
|
+
{
|
|
27
|
+
return T{std::forward<U>(value)};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
template <typename T>
|
|
31
|
+
[[nodiscard]] std::optional<T> optional_value(std::nullopt_t)
|
|
32
|
+
{
|
|
33
|
+
return std::nullopt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
template <typename T, typename... Args>
|
|
37
|
+
[[nodiscard]] std::vector<std::optional<T>> values(Args &&...args)
|
|
38
|
+
{
|
|
39
|
+
std::vector<std::optional<T>> output;
|
|
40
|
+
output.reserve(sizeof...(Args));
|
|
41
|
+
(output.push_back(optional_value<T>(std::forward<Args>(args))), ...);
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
void require(bool condition, std::string message)
|
|
46
|
+
{
|
|
47
|
+
if (!condition) { throw std::runtime_error(std::move(message)); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void require_output(
|
|
51
|
+
const std::vector<std::optional<Float>> &actual,
|
|
52
|
+
const std::vector<std::optional<Float>> &expected,
|
|
53
|
+
const std::string &label)
|
|
54
|
+
{
|
|
55
|
+
require(actual.size() == expected.size(), label + ": output size");
|
|
56
|
+
for (std::size_t index = 0; index < expected.size(); ++index)
|
|
57
|
+
{
|
|
58
|
+
require(actual[index].has_value() == expected[index].has_value(),
|
|
59
|
+
label + ": readiness at index " + std::to_string(index));
|
|
60
|
+
if (expected[index].has_value())
|
|
61
|
+
{
|
|
62
|
+
require(std::abs(*actual[index] - *expected[index]) <= 1.0e-12,
|
|
63
|
+
label + ": value at index " + std::to_string(index));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
template <typename T>
|
|
69
|
+
void require_value_output(
|
|
70
|
+
const std::vector<std::optional<Value>> &actual,
|
|
71
|
+
const std::vector<std::optional<T>> &expected,
|
|
72
|
+
const std::string &label)
|
|
73
|
+
{
|
|
74
|
+
require(actual.size() == expected.size(), label + ": output size");
|
|
75
|
+
for (std::size_t index = 0; index < expected.size(); ++index)
|
|
76
|
+
{
|
|
77
|
+
require(actual[index].has_value() == expected[index].has_value(),
|
|
78
|
+
label + ": readiness at index " + std::to_string(index));
|
|
79
|
+
if (!expected[index].has_value()) { continue; }
|
|
80
|
+
const T value = actual[index]->view().checked_as<T>();
|
|
81
|
+
if constexpr (std::is_floating_point_v<T>)
|
|
82
|
+
{
|
|
83
|
+
require(std::abs(value - *expected[index]) <= 1.0e-12,
|
|
84
|
+
label + ": value at index " + std::to_string(index));
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
{
|
|
88
|
+
require(value == *expected[index],
|
|
89
|
+
label + ": value at index " + std::to_string(index));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
struct DefaultIntChange
|
|
95
|
+
{
|
|
96
|
+
static constexpr auto name = "analytics_default_int_change";
|
|
97
|
+
|
|
98
|
+
static Port<TS<Float>> compose(Wiring &w, Port<TS<Int>> ts)
|
|
99
|
+
{
|
|
100
|
+
return wire<pct_change, TS<Float>>(w, ts);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
struct TwoPeriodFloatChange
|
|
105
|
+
{
|
|
106
|
+
static constexpr auto name = "analytics_two_period_float_change";
|
|
107
|
+
|
|
108
|
+
static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
|
|
109
|
+
{
|
|
110
|
+
return wire<pct_change, TS<Float>>(w, ts, Int{2});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
template <stdlib::DivideByZero Policy>
|
|
115
|
+
struct ZeroPolicyChange
|
|
116
|
+
{
|
|
117
|
+
static constexpr auto name = "analytics_zero_policy_change";
|
|
118
|
+
|
|
119
|
+
static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
|
|
120
|
+
{
|
|
121
|
+
return wire<pct_change, TS<Float>>(w, ts, Int{1}, Policy);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
struct InvalidPeriodChange
|
|
126
|
+
{
|
|
127
|
+
static constexpr auto name = "analytics_invalid_period_change";
|
|
128
|
+
|
|
129
|
+
static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
|
|
130
|
+
{
|
|
131
|
+
return wire<pct_change, TS<Float>>(w, ts, Int{0});
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
void test_default_and_sparse_observations()
|
|
136
|
+
{
|
|
137
|
+
require_output(eval_node<DefaultIntChange>(values<Int>(1, 2, 3)),
|
|
138
|
+
{std::nullopt, 1.0, 0.5}, "default period");
|
|
139
|
+
|
|
140
|
+
require_output(eval_node<DefaultIntChange>(values<Int>(1, none, 2)),
|
|
141
|
+
{std::nullopt, std::nullopt, 1.0}, "sparse observations");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
void test_period_and_float_input()
|
|
145
|
+
{
|
|
146
|
+
require_output(
|
|
147
|
+
eval_node<TwoPeriodFloatChange>(
|
|
148
|
+
values<Float>(10.0, 11.0, 12.0, 15.0)),
|
|
149
|
+
{std::nullopt, std::nullopt, 0.2, 4.0 / 11.0},
|
|
150
|
+
"two-period change");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
void test_zero_policies()
|
|
154
|
+
{
|
|
155
|
+
const auto nan = eval_node<ZeroPolicyChange<stdlib::DivideByZero::Nan>>(
|
|
156
|
+
values<Float>(0.0, 1.0));
|
|
157
|
+
require(nan.size() == 2 && !nan[0].has_value() && nan[1].has_value(),
|
|
158
|
+
"NaN policy output readiness");
|
|
159
|
+
require(std::isnan(*nan[1]),
|
|
160
|
+
"NaN policy result");
|
|
161
|
+
|
|
162
|
+
require_output(
|
|
163
|
+
eval_node<ZeroPolicyChange<stdlib::DivideByZero::Zero>>(
|
|
164
|
+
values<Float>(0.0, 1.0)),
|
|
165
|
+
{std::nullopt, 0.0}, "zero policy");
|
|
166
|
+
require_output(
|
|
167
|
+
eval_node<ZeroPolicyChange<stdlib::DivideByZero::One>>(
|
|
168
|
+
values<Float>(0.0, 1.0)),
|
|
169
|
+
{std::nullopt, 1.0}, "one policy");
|
|
170
|
+
require_output(
|
|
171
|
+
eval_node<ZeroPolicyChange<stdlib::DivideByZero::NoTick>>(
|
|
172
|
+
values<Float>(0.0, 1.0)),
|
|
173
|
+
{std::nullopt, std::nullopt}, "no-tick policy");
|
|
174
|
+
|
|
175
|
+
bool raised = false;
|
|
176
|
+
try
|
|
177
|
+
{
|
|
178
|
+
static_cast<void>(
|
|
179
|
+
eval_node<ZeroPolicyChange<stdlib::DivideByZero::Error>>(
|
|
180
|
+
values<Float>(0.0, 1.0)));
|
|
181
|
+
}
|
|
182
|
+
catch (const std::exception &)
|
|
183
|
+
{
|
|
184
|
+
raised = true;
|
|
185
|
+
}
|
|
186
|
+
require(raised, "Error policy rejects a zero prior value");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
void test_invalid_period()
|
|
190
|
+
{
|
|
191
|
+
bool raised = false;
|
|
192
|
+
try
|
|
193
|
+
{
|
|
194
|
+
static_cast<void>(eval_node<InvalidPeriodChange>(values<Float>(1.0)));
|
|
195
|
+
}
|
|
196
|
+
catch (const std::invalid_argument &)
|
|
197
|
+
{
|
|
198
|
+
raised = true;
|
|
199
|
+
}
|
|
200
|
+
require(raised, "non-positive period is rejected while wiring");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
void test_migrated_analytical_operators()
|
|
204
|
+
{
|
|
205
|
+
require_value_output(eval_node<diff>(values<Int>(1, 2, 4, 7)),
|
|
206
|
+
values<Int>(none, 1, 2, 3), "integer diff");
|
|
207
|
+
require_value_output(eval_node<diff>(values<Float>(1.0, 1.5, 3.0)),
|
|
208
|
+
values<Float>(none, 0.5, 1.5), "floating diff");
|
|
209
|
+
require_value_output(eval_node<count>(values<Int>(3, none, 2, 1)),
|
|
210
|
+
values<Int>(1, none, 2, 3), "count");
|
|
211
|
+
require_value_output(eval_node<count>(values<Int>(3, 2, 1),
|
|
212
|
+
values<Bool>(none, true, none)),
|
|
213
|
+
values<Int>(1, 1, 2), "reset count");
|
|
214
|
+
require_value_output(eval_node<clip>(values<Int>(-1, 1, 3),
|
|
215
|
+
Int{0}, Int{2}),
|
|
216
|
+
values<Int>(0, 1, 2), "integer clip");
|
|
217
|
+
require_value_output(eval_node<clip>(values<Float>(-1.0, 0.5, 2.0),
|
|
218
|
+
Float{0.0}, Float{1.0}),
|
|
219
|
+
values<Float>(0.0, 0.5, 1.0), "floating clip");
|
|
220
|
+
require_value_output(eval_node<ewma>(values<Float>(1.0, 2.0, 3.0, 4.0),
|
|
221
|
+
Float{0.5}),
|
|
222
|
+
values<Float>(1.0, 1.5, 2.25, 3.125), "ewma");
|
|
223
|
+
|
|
224
|
+
bool raised = false;
|
|
225
|
+
try
|
|
226
|
+
{
|
|
227
|
+
static_cast<void>(eval_node<clip>(
|
|
228
|
+
values<Float>(1.0), Float{1.0}, Float{-1.0}));
|
|
229
|
+
}
|
|
230
|
+
catch (const std::exception &)
|
|
231
|
+
{
|
|
232
|
+
raised = true;
|
|
233
|
+
}
|
|
234
|
+
require(raised, "clip rejects reversed bounds");
|
|
235
|
+
}
|
|
236
|
+
} // namespace
|
|
237
|
+
|
|
238
|
+
int main()
|
|
239
|
+
{
|
|
240
|
+
try
|
|
241
|
+
{
|
|
242
|
+
hgraph::stdlib::register_standard_operators();
|
|
243
|
+
hgraph::analytics::register_analytics_operators();
|
|
244
|
+
test_default_and_sparse_observations();
|
|
245
|
+
test_period_and_float_input();
|
|
246
|
+
test_zero_policies();
|
|
247
|
+
test_invalid_period();
|
|
248
|
+
test_migrated_analytical_operators();
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
catch (const std::exception &error)
|
|
252
|
+
{
|
|
253
|
+
std::cerr << error.what() << '\n';
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Audit hgraph-analytics wheels and source distributions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import glob
|
|
8
|
+
import itertools
|
|
9
|
+
import tarfile
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
WHEEL_REQUIRED = (
|
|
14
|
+
"hgraph_analytics/__init__.py",
|
|
15
|
+
"hgraph_analytics/py.typed",
|
|
16
|
+
"include/hgraph/analytics/operators.h",
|
|
17
|
+
# GNUInstallDirs selects lib64 on manylinux, while macOS and Windows wheels
|
|
18
|
+
# use lib. Match the package-relative suffix so both layouts are audited.
|
|
19
|
+
"cmake/hgraph-analytics/hgraph-analyticsConfig.cmake",
|
|
20
|
+
"cmake/hgraph-analytics/hgraphAnalyticsTargets.cmake",
|
|
21
|
+
)
|
|
22
|
+
SDIST_REQUIRED = (
|
|
23
|
+
"CMakeLists.txt",
|
|
24
|
+
"cmake/hgraph-analyticsConfig.cmake.in",
|
|
25
|
+
"include/hgraph/analytics/operators.h",
|
|
26
|
+
"python/hgraph_analytics/__init__.py",
|
|
27
|
+
"python/hgraph_analytics/py.typed",
|
|
28
|
+
"src/operators.cpp",
|
|
29
|
+
"test_package/CMakeLists.txt",
|
|
30
|
+
"tests/test_pct_change.cpp",
|
|
31
|
+
"tools/audit_distribution.py",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _assert_required(names: set[str], required: tuple[str, ...], path: Path) -> None:
|
|
36
|
+
for item in required:
|
|
37
|
+
if not any(name.endswith(f"/{item}") or name == item for name in names):
|
|
38
|
+
raise AssertionError(f"{path}: missing {item}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _audit_wheel(path: Path) -> None:
|
|
42
|
+
with zipfile.ZipFile(path) as archive:
|
|
43
|
+
names = set(archive.namelist())
|
|
44
|
+
metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")]
|
|
45
|
+
if len(metadata_names) != 1:
|
|
46
|
+
raise AssertionError(f"{path}: expected one METADATA file, got {metadata_names}")
|
|
47
|
+
metadata = archive.read(metadata_names[0]).decode()
|
|
48
|
+
|
|
49
|
+
_assert_required(names, WHEEL_REQUIRED, path)
|
|
50
|
+
native_modules = [
|
|
51
|
+
name
|
|
52
|
+
for name in names
|
|
53
|
+
if name.startswith("hgraph_analytics/_hgraph_analytics.")
|
|
54
|
+
and name.endswith((".so", ".pyd"))
|
|
55
|
+
]
|
|
56
|
+
if len(native_modules) != 1:
|
|
57
|
+
raise AssertionError(
|
|
58
|
+
f"{path}: expected one native hgraph_analytics module, got {native_modules}"
|
|
59
|
+
)
|
|
60
|
+
analytics_libraries = [
|
|
61
|
+
name
|
|
62
|
+
for name in names
|
|
63
|
+
if Path(name).name.startswith(("libhgraph_analytics", "hgraph_analytics"))
|
|
64
|
+
and Path(name).suffix in {".a", ".lib"}
|
|
65
|
+
]
|
|
66
|
+
if len(analytics_libraries) != 1:
|
|
67
|
+
raise AssertionError(
|
|
68
|
+
f"{path}: expected one native analytics library, got {analytics_libraries}"
|
|
69
|
+
)
|
|
70
|
+
forbidden = [
|
|
71
|
+
name
|
|
72
|
+
for name in names
|
|
73
|
+
if Path(name).name.startswith(
|
|
74
|
+
(
|
|
75
|
+
"hgraph_runtime",
|
|
76
|
+
"hgraph_stdlib",
|
|
77
|
+
"hgraph_wiring",
|
|
78
|
+
"libhgraph_runtime",
|
|
79
|
+
"libhgraph_stdlib",
|
|
80
|
+
"libhgraph_wiring",
|
|
81
|
+
"libnanobind-abi3",
|
|
82
|
+
"nanobind-abi3",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
]
|
|
86
|
+
if forbidden:
|
|
87
|
+
raise AssertionError(f"{path}: embeds core runtime libraries: {forbidden}")
|
|
88
|
+
if "Requires-Dist: hgraph" not in metadata:
|
|
89
|
+
raise AssertionError(f"{path}: does not declare the core distribution dependency")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _audit_sdist(path: Path) -> None:
|
|
93
|
+
with tarfile.open(path, "r:gz") as archive:
|
|
94
|
+
names = set(archive.getnames())
|
|
95
|
+
_assert_required(names, SDIST_REQUIRED, path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _audit_distribution(path: Path) -> None:
|
|
99
|
+
if path.suffix == ".whl":
|
|
100
|
+
_audit_wheel(path)
|
|
101
|
+
elif path.name.endswith(".tar.gz"):
|
|
102
|
+
_audit_sdist(path)
|
|
103
|
+
else:
|
|
104
|
+
raise SystemExit(f"unsupported distribution: {path}")
|
|
105
|
+
print(f"audited {path}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main() -> None:
|
|
109
|
+
arguments = argparse.ArgumentParser()
|
|
110
|
+
arguments.add_argument("distributions", nargs="+")
|
|
111
|
+
patterns = arguments.parse_args().distributions
|
|
112
|
+
matches = itertools.chain.from_iterable(glob.iglob(pattern) for pattern in patterns)
|
|
113
|
+
paths = sorted(map(Path, set(matches)))
|
|
114
|
+
if not paths:
|
|
115
|
+
raise SystemExit("no hgraph-analytics distributions matched")
|
|
116
|
+
for distribution in paths:
|
|
117
|
+
_audit_distribution(distribution)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
main()
|