diff --git a/graphblas/_ss/matrix.py b/graphblas/_ss/matrix.py index a12ea16a1..5d448eb2a 100644 --- a/graphblas/_ss/matrix.py +++ b/graphblas/_ss/matrix.py @@ -442,6 +442,8 @@ def build_diag(self, vector, k=0): vector = self._parent._expect_type( vector, gb.Vector, within="ss.build_diag", argname="vector" ) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call("GxB_Matrix_diag", [self._parent, vector, _as_scalar(k, INT64, is_cscalar=True), None]) def split(self, chunks, *, name=None): @@ -544,6 +546,8 @@ def concat(self, tiles): graphblas.ss.concat """ tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=True) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) self._concat(tiles, m, n) def build_scalar(self, rows, columns, value): @@ -564,6 +568,8 @@ def build_scalar(self, rows, columns, value): f"`rows` and `columns` lengths must match: {rows.size}, {columns.size}" ) scalar = _as_scalar(value, self._parent.dtype, is_cscalar=False) # pragma: is_grbscalar + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call( "GxB_Matrix_build_Scalar", [ @@ -897,8 +903,12 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m format = f"{self.format[:-1]}r" elif format == "columnwise": format = f"{self.format[:-1]}c" - if give_ownership or format == "coo": + if format == "coo": parent = self._parent + elif give_ownership: + parent = self._parent + if parent._hooks is not None and "onchange" in parent._hooks: + parent._hooks["onchange"](self) else: parent = self._parent.dup(name=f"M_{method}") dtype = parent.dtype.np_type @@ -1388,6 +1398,8 @@ def pack_csr( See `Matrix.ss.import_csr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_csr( indptr=indptr, values=values, @@ -1561,6 +1573,8 @@ def pack_csc( See `Matrix.ss.import_csc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_csc( indptr=indptr, values=values, @@ -1744,6 +1758,8 @@ def pack_hypercsr( See `Matrix.ss.import_hypercsr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_hypercsr( rows=rows, indptr=indptr, @@ -1938,6 +1954,8 @@ def pack_hypercsc( See `Matrix.ss.import_hypercsc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_hypercsc( cols=cols, indptr=indptr, @@ -2126,6 +2144,8 @@ def pack_bitmapr( See `Matrix.ss.import_bitmapr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmapr( bitmap=bitmap, values=values, @@ -2302,6 +2322,8 @@ def pack_bitmapc( See `Matrix.ss.import_bitmapc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmapc( bitmap=bitmap, values=values, @@ -2467,6 +2489,8 @@ def pack_fullr( See `Matrix.ss.import_fullr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_fullr( values=values, is_iso=is_iso, @@ -2614,6 +2638,8 @@ def pack_fullc( See `Matrix.ss.import_fullc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_fullc( values=values, is_iso=is_iso, @@ -2765,6 +2791,8 @@ def pack_coo( See `Matrix.ss.import_coo` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_coo( nrows=self._parent._nrows, ncols=self._parent._ncols, @@ -2943,6 +2971,8 @@ def pack_coor( See `Matrix.ss.import_coor` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_coor( rows=rows, cols=cols, @@ -3096,6 +3126,8 @@ def pack_cooc( See `Matrix.ss.import_cooc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_cooc( ncols=self._parent._ncols, rows=rows, @@ -3284,6 +3316,8 @@ def pack_any( See `Matrix.ss.import_any` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_any( values=values, is_iso=is_iso, diff --git a/graphblas/_ss/vector.py b/graphblas/_ss/vector.py index 346ab4c38..ebe5ab1e2 100644 --- a/graphblas/_ss/vector.py +++ b/graphblas/_ss/vector.py @@ -169,6 +169,8 @@ def build_diag(self, matrix, k=0): # Transpose descriptor doesn't do anything, so use the parent k = -k matrix = matrix._matrix + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call("GxB_Vector_diag", [self._parent, matrix, _as_scalar(k, INT64, is_cscalar=True), None]) def split(self, chunks, *, name=None): @@ -253,6 +255,8 @@ def concat(self, tiles): graphblas.ss.concat """ tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=False) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) self._concat(tiles, m) def build_scalar(self, indices, value): @@ -268,6 +272,8 @@ def build_scalar(self, indices, value): """ indices = ints_to_numpy_buffer(indices, np.uint64, name="indices") scalar = _as_scalar(value, self._parent.dtype, is_cscalar=False) # pragma: is_grbscalar + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call( "GxB_Vector_build_Scalar", [ @@ -464,6 +470,8 @@ def unpack(self, format=None, *, sort=False, raw=False): def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method): if give_ownership: parent = self._parent + if parent._hooks is not None and "onchange" in parent._hooks: + parent._hooks["onchange"](self) else: parent = self._parent.dup(name=f"v_{method}") dtype = parent.dtype.np_type @@ -680,6 +688,8 @@ def pack_any( See `Vector.ss.import_any` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_any( values=values, is_iso=is_iso, @@ -860,6 +870,8 @@ def pack_sparse( See `Vector.ss.import_sparse` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_sparse( indices=indices, values=values, @@ -1027,6 +1039,8 @@ def pack_bitmap( See `Vector.ss.import_bitmap` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmap( bitmap=bitmap, values=values, @@ -1188,6 +1202,8 @@ def pack_full( See `Vector.ss.import_full` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_full( values=values, is_iso=is_iso, diff --git a/graphblas/base.py b/graphblas/base.py index 491c9b31f..aa14a6a08 100644 --- a/graphblas/base.py +++ b/graphblas/base.py @@ -329,6 +329,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): "Scalar accumulation with extract element" "--such as `s(accum=accum) << v[0]`--is not supported" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) expr.parent._extract_element( expr.resolved_indexes, self.dtype, is_cscalar=self._is_cscalar, result=self ) @@ -396,8 +398,9 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): if type(expr) is Scalar: scalar = expr else: + dtype = self.dtype if self.dtype._is_udt else None try: - scalar = Scalar.from_value(expr, is_cscalar=None, name="") + scalar = Scalar.from_value(expr, dtype, is_cscalar=None, name="") except TypeError: raise TypeError( "Assignment value must be a valid expression type, not " @@ -422,10 +425,14 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): if input_mask is not None: raise TypeError("`input_mask` argument may only be used for extract") elif expr.op is not None and expr.op.opclass == "Aggregator": + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) updater = self(mask=mask, accum=accum, replace=replace) expr.op._new(updater, expr) return elif expr.cfunc_name is None: # Custom recipe + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) updater = self(mask=mask, accum=accum, replace=replace) expr.args[-2](updater, *expr.args[-1]) return @@ -474,6 +481,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): args.append(expr.op) args.extend(expr.args) args.append(desc) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) # Make the GraphBLAS call call(cfunc_name, args) if self._is_scalar: diff --git a/graphblas/expr.py b/graphblas/expr.py index c12f97fa1..bdb3ef718 100644 --- a/graphblas/expr.py +++ b/graphblas/expr.py @@ -406,6 +406,8 @@ def __getitem__(self, keys): def _setitem(self, resolved_indexes, obj, *, is_submask): # Occurs when user calls C(params)[index] = expr + if self.parent._hooks is not None and "onchange" in self.parent._hooks: + self.parent._hooks["onchange"](self) if resolved_indexes.is_single_element and not self.kwargs: # Fast path using assignElement self.parent._assign_element(resolved_indexes, obj) @@ -427,6 +429,8 @@ def __delitem__(self, keys): if self.parent._is_scalar: raise TypeError("Indexing not supported for Scalars") resolved_indexes = IndexerResolver(self.parent, keys) + if self.parent._hooks is not None and "onchange" in self.parent._hooks: + self.parent._hooks["onchange"](self) if resolved_indexes.is_single_element: self.parent._delete_element(resolved_indexes) else: diff --git a/graphblas/matrix.py b/graphblas/matrix.py index 9d6595219..6847e1cf1 100644 --- a/graphblas/matrix.py +++ b/graphblas/matrix.py @@ -62,7 +62,7 @@ class Matrix(BaseType): High-level wrapper around GrB_Matrix type """ - __slots__ = "_nrows", "_ncols", "_parent", "ss" + __slots__ = "_nrows", "_ncols", "_parent", "_hooks", "ss" ndim = 2 _is_transposed = False _name_counter = itertools.count() @@ -78,6 +78,7 @@ def __new__(cls, dtype=FP64, nrows=0, ncols=0, *, name=None): self._nrows = nrows.value self._ncols = ncols.value self._parent = None + self._hooks = None self.ss = ss(self) return self @@ -90,6 +91,7 @@ def _from_obj(cls, gb_obj, dtype, nrows, ncols, *, parent=None, name=None): self._nrows = nrows self._ncols = ncols self._parent = parent + self._hooks = None if parent is None else parent._hooks self.ss = ss(self) return self @@ -271,11 +273,15 @@ def T(self): return TransposedMatrix(self) def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Matrix_clear", [self]) def resize(self, nrows, ncols): nrows = _as_scalar(nrows, _INDEX, is_cscalar=True) ncols = _as_scalar(ncols, _INDEX, is_cscalar=True) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Matrix_resize", [self, nrows, ncols]) self._nrows = nrows.value self._ncols = ncols.value @@ -325,6 +331,8 @@ def build(self, rows, columns, values, *, dup_op=None, clear=False, nrows=None, f"`rows` and `columns` and `values` lengths must match: " f"{rows.size}, {columns.size}, {values.size}" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if clear: self.clear() if nrows is not None or ncols is not None: diff --git a/graphblas/scalar.py b/graphblas/scalar.py index 478d5e4f9..1d3f9a373 100644 --- a/graphblas/scalar.py +++ b/graphblas/scalar.py @@ -23,6 +23,7 @@ def _scalar_index(name): self.gb_obj = ffi_new("GrB_Index*") self._is_cscalar = True self._empty = True + self._hooks = None return self @@ -32,7 +33,7 @@ class Scalar(BaseType): Pseudo-object for GraphBLAS functions which accumulate into a scalar type """ - __slots__ = "_empty", "_is_cscalar" + __slots__ = "_empty", "_is_cscalar", "_hooks" ndim = 0 shape = () _is_scalar = True @@ -56,6 +57,7 @@ def __new__(cls, dtype=FP64, *, is_cscalar=False, name=None): else: self.gb_obj = ffi_new("GrB_Scalar*") call("GrB_Scalar_new", [_Pointer(self), dtype]) + self._hooks = None return self @classmethod @@ -65,6 +67,7 @@ def _from_obj(cls, gb_obj, dtype, *, is_cscalar=False, name=None): self.gb_obj = gb_obj self.dtype = dtype self._is_cscalar = is_cscalar + self._hooks = None return self def __del__(self): @@ -167,8 +170,8 @@ def isequal(self, other, *, check_dtype=False): if type(other) is not Scalar: if other is None: return self._is_empty + dtype = self.dtype if self.dtype._is_udt else None try: - dtype = self.dtype if self.dtype._is_udt else None other = Scalar.from_value(other, dtype, is_cscalar=None, name="s_isequal") except TypeError: other = self._expect_type( @@ -204,8 +207,9 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False): if type(other) is not Scalar: if other is None: return self._is_empty + dtype = self.dtype if self.dtype._is_udt else None try: - other = Scalar.from_value(other, is_cscalar=None, name="s_isclose") + other = Scalar.from_value(other, dtype, is_cscalar=None, name="s_isclose") except TypeError: other = self._expect_type( other, @@ -235,6 +239,8 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False): return isclose_func._numba_func(self.value, other.value) def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if self._is_empty: return if self._is_cscalar: @@ -278,6 +284,8 @@ def value(self): @value.setter def value(self, val): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if val is None or output_type(val) is Scalar and val._is_empty: self.clear() elif self._is_cscalar: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 38eb4c3f3..003a0ce10 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2739,6 +2739,7 @@ def test_expr_is_like_matrix(A): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -2781,6 +2782,7 @@ def test_index_expr_is_like_matrix(A): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -3586,3 +3588,58 @@ def test_ss_serialize(A): A.ss.serialize("lz4hc", 0) with pytest.raises(InvalidObject): Matrix.ss.deserialize(a[:-5]) + + +def test_hooks(A): + B = A.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + B._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + B.clear() + with pytest.raises(OnChangeException): + B.resize(100, 100) + with pytest.raises(OnChangeException): + B.build([1, 2], [1, 2], [1, 2], clear=True) + with pytest.raises(OnChangeException): + B[1, 2] = 2 + with pytest.raises(OnChangeException): + del B[1, 1] + with pytest.raises(OnChangeException): + B[:, :] = A + with pytest.raises(OnChangeException): + del B[[1, 2], [1, 2]] + with pytest.raises(OnChangeException): + B << B.reposition(1, 2) + with pytest.raises(OnChangeException): + B << A @ A + v = A.diag() + with pytest.raises(OnChangeException): + B.ss.build_diag(v) + tiles = B.ss.split((3, 3)) + with pytest.raises(OnChangeException): + B.ss.concat(tiles) + with pytest.raises(OnChangeException): + B << B + 1 + with pytest.raises(OnChangeException): + B.ss.build_scalar([1, 2], [1, 2], 1) + with pytest.raises(OnChangeException): + B.ss.export(give_ownership=True) + with pytest.raises(OnChangeException): + B.ss.unpack() + info = B.ss.export() + with pytest.raises(OnChangeException): + B.ss.pack_any(**info) + for fmt in ["csr", "csc", "hypercsr", "hypercsc", "bitmapr", "bitmapc", "coor", "cooc", "coo"]: + info = B.ss.export(fmt) + with pytest.raises(OnChangeException): + getattr(B.ss, f"pack_{fmt}")(**info) + with pytest.raises(OnChangeException): + B.ss.pack_fullr(np.ones(B.shape)) + with pytest.raises(OnChangeException): + B.ss.pack_fullc(np.ones(B.shape)) diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 8b0b21e1d..6ee688efa 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -319,6 +319,7 @@ def test_expr_is_like_scalar(s): "_expr_name", "_expr_name_html", "_from_obj", + "_hooks", "_name_counter", "_update", "clear", @@ -351,6 +352,7 @@ def test_index_expr_is_like_scalar(s): "_expr_name", "_expr_name_html", "_from_obj", + "_hooks", "_name_counter", "_update", "clear", @@ -458,3 +460,28 @@ def test_get(s): s.clear() assert compute(s.get()) is None assert s.get("mittens") == "mittens" + + +def test_hooks(s): + t = s.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + t._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + t.clear() + with pytest.raises(OnChangeException): + t.value = 7 + v = Vector.from_values([1, 2], [3, 4]) + with pytest.raises(OnChangeException): + t << v[1] + with pytest.raises(OnChangeException): + t << v.reduce() + with pytest.raises(OnChangeException): + t << v.reduce(gb.agg.count) + with pytest.raises(OnChangeException): + t << s diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index bc3d4b9df..626d6bedf 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -1547,6 +1547,7 @@ def test_expr_is_like_vector(v): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -1582,6 +1583,7 @@ def test_index_expr_is_like_vector(v): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -2249,3 +2251,60 @@ def test_ss_serialize(v): v.ss.serialize("lz4hc", 0) with pytest.raises(InvalidObject): Vector.ss.deserialize(a[:-5]) + + +def test_hooks(v): + w = v.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + w._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + w.clear() + with pytest.raises(OnChangeException): + w.resize(100) + with pytest.raises(OnChangeException): + w.build([1, 2], [1, 2], clear=True) + with pytest.raises(OnChangeException): + w[1] = 2 + with pytest.raises(OnChangeException): + del w[1] + with pytest.raises(OnChangeException): + w[: v.size] = v + with pytest.raises(OnChangeException): + del w[[1, 2]] + with pytest.raises(OnChangeException): + w << w.reposition(1) + A = w.outer(w).new() + with pytest.raises(OnChangeException): + w << A.reduce_rowwise(agg.count) + with pytest.raises(OnChangeException): + w.ss.build_diag(A) + tiles = w.ss.split(3) + with pytest.raises(OnChangeException): + w.ss.concat(tiles) + with pytest.raises(OnChangeException): + w << w + w + with pytest.raises(OnChangeException): + w << w + 1 + with pytest.raises(OnChangeException): + w.ss.build_scalar([1, 2, 3], 1) + with pytest.raises(OnChangeException): + w.ss.export(give_ownership=True) + with pytest.raises(OnChangeException): + w.ss.unpack() + info = w.ss.export() + with pytest.raises(OnChangeException): + w.ss.pack_any(**info) + info = w.ss.export("sparse") + with pytest.raises(OnChangeException): + w.ss.pack_sparse(**info) + info = w.ss.export("bitmap") + with pytest.raises(OnChangeException): + w.ss.pack_bitmap(**info) + with pytest.raises(OnChangeException): + w.ss.pack_full(np.arange(w.size)) diff --git a/graphblas/vector.py b/graphblas/vector.py index 5630e83e4..e3f510f02 100644 --- a/graphblas/vector.py +++ b/graphblas/vector.py @@ -77,7 +77,7 @@ class Vector(BaseType): High-level wrapper around GrB_Vector type """ - __slots__ = "_size", "_parent", "ss" + __slots__ = "_size", "_parent", "_hooks", "ss" ndim = 1 _name_counter = itertools.count() @@ -90,6 +90,7 @@ def __new__(cls, dtype=FP64, size=0, *, name=None): call("GrB_Vector_new", [_Pointer(self), self.dtype, size]) self._size = size.value self._parent = None + self._hooks = None self.ss = ss(self) return self @@ -101,6 +102,7 @@ def _from_obj(cls, gb_obj, dtype, size, *, parent=None, name=None): self.dtype = dtype self._size = size self._parent = parent + self._hooks = None if parent is None else parent._hooks self.ss = ss(self) return self @@ -281,10 +283,14 @@ def _nvals(self): return n[0] def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Vector_clear", [self]) def resize(self, size): size = _as_scalar(size, _INDEX, is_cscalar=True) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Vector_resize", [self, size]) self._size = size.value @@ -325,6 +331,8 @@ def build(self, indices, values, *, dup_op=None, clear=False, size=None): raise ValueError( f"`indices` and `values` lengths must match: {indices.size} != {values.size}" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if clear: self.clear() if size is not None: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy