Name | easypoint JSON |
Version |
0.2.0
JSON |
| download |
home_page | |
Summary | Minimal general-purpose vector / matrix arithmetics library |
upload_time | 2023-07-28 09:07:54 |
maintainer | |
docs_url | None |
author | Dmitry Gritsenko |
requires_python | >=3.7,<4.0 |
license | MIT |
keywords |
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# Minimal general-purpose vector / matrix arithmetics library
# Installation
Install `easypoint` module:
```bash
poetry add easypoint
```
or
```bash
python -m pip install easypoint
```
# Introduction
easypoint has 2 main types to work with: `Point` (a.k.a. `Vector`) and `Matrix`
`Point` class builds up on my previous work with [evtn/soda](https://github.com/evtn/soda) and [evtn/soda-old](https://github.com/evtn/soda-old).
Both being graphics-oriented, so vector arithmetics is a must-have.
But over time, `Point` became a convenient class for various non-graphical tasks and tasks out of scope for `soda` (e.g. raster graphics).
This module also brings a refined `Matrix` class I've been using in various private/unfinished projects (an old version can be seen [here](https://gist.github.com/evtn/8683e58770f2901527275d46465e4cbe))
Both are refined and generalized for N dimensions. Some new additions (like `Point.transform(matrix: Matrix)`) are also in place.
# Usage
## Point
Point/Vector (`easypoint.Vector` is just an alias) is a fancy `Sequence[float]`, supporting various convenient operations.
### Make a point
```python
from easypoint import Point
# create a Point with numbers:
p1 = Point(1, 2, 3) # Point[1, 2, 3]
# ...or from a list/tuple
p2 = Point.from_([1, 2, 3]) # Point[1, 2, 3]
# ...from a number
p3 = Point.from_(1) # Point[1, ...]
# ...from another Point
p4 = p1[:2] # Point[1, 2]
p5 = p1[0, 2, 1] # Point[1, 3, 2]
p6 = p3[:] # Error (a slice of an infinite Point)
```
```python
a = Point(1, 2)
b = Point(4, 5)
```
In any context where a point could be used, "point-like" values can be used:
- `(1, 2)` <-> `Point(1, 2)`
- `[1, 2]` <-> `Point(1, 2)`
- `1` <-> `Point(1, loop=True)`
### Math
You can perform mathematical operations on points (element-wise):
```python
a + b # Point[5, 7]
a - b # Point[-3, -3]
a * b # Point[4, 10]
a / b # Point[0.25, 0.4]
a % b # Point[1, 2]
```
...and any point-like values:
```python
a + 10 # Point[11, 12]
a * 2 # Point[2, 4]
```
### Distance
You also can calculate distance between points and get a normalized vector:
```python
from math import pi
a.distance(b) # 4.242640687119285
a.distance() # 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)
a.normalized() # Point[0.4472135954999579, 0.8944271909999159]
```
### Rotation
2D Rotation can be done around some center:
```python
a.rotate2d(degrees=90) # Point[-2, 1]
a.rotate2d(center=(10, 10), radians=pi / 2) # Point[18, 1]
a.rotate2d(center=10, degrees=90) # Point[18, 1]
# if you want to use axis other than (0, 1), pass `axis`:
c = Point(4, 6, 2, 3, 2)
c.rotate2d(center=10, radians=pi / 2, axis=(3, 4)) # Point[4, 6, 2, 18, 3]
```
### Transforms
You can transform an N-dimensional `Point` with a NxN `Matrix`:
```python
from easypoint import Point, Matrix
# Shearing
# 1 k
# 0 1
matrix = Matrix.as_matrix((1, 4), (0, 1))
t = Point(0, 5)
t.transform(matrix) # Point[20, 5]
```
### Looped points
Sometimes it's convenient to have a point with `p[i] == p[i % n]` (a repeating set of coordinates).
It can be achieved by passing `loop=True` into `Point` constructor or `Point.from_`:
```python
p1 = Point(10.3, loop=True) # Point[10.3, ...]
p1[54378] # 10.3
p2 = Point.from_([1, 2, 3], loop=True) # Point[1, 2, 3, ...]
p2[540:550] # Point[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
```
Keep in mind that `Point.from_(int)` always produces a looped point, if you need a 1-dimensional point, use `Point(int)`
### Indexing
Points support three types of indexing:
- `point[int]` returns a value at that index, or 0 if this index doesn't exist (and the point is not looped)
- `point[slice]` returns a `Point` with values under that slice
- `point[tuple[int, ...]]` returns a Point with values under indices in the tuple
```python
a = Point(*range(5)) # Point[0, 1, 2, 3, 4]
a[2] # 2
a[2:4] # Point[2, 3, 4]
a[4, 3, 8, 2] # Point[4, 3, 0, 2]
```
Same applies for setting values on indices.
Keep in mind that setting a slice/tuple doesn't change the dimension count, extra indices/values are ignored
There are also `x`, `y`, and `z` properties as aliases for `[0]`, `[1]`, and `[2]`
### Interpolation
For convenience, there are `point.interpolate(other: PointLike, k: float)` to interpolate between two points (self at 0, other at 1).
`point.center(other: PointLike)` is an alias for `point.interpolate(other, 0.5)`
### Naming
You can give any point a name (any string) for convenience and better output:
```python
a = Point(3, 4) # Point[3, 4]
b = a.named("B") # Point<B>[3, 4]
```
Naming returns a copy of the point, so the original one is not renamed
### FnPoint
FnPoint is a Point defined by index function:
```python
from easypoint import FnPoint
fp = FnPoint(lambda i: 126 * i * i + 7)
fp[4] # 2023
```
...and optional length:
```python
from easypoint import FnPoint
fp = FnPoint(lambda i: 126 * i * i + 7, length=3)
fp[4] # 0
```
It is fully compatible with Point, but any operation on FnPoint will return you a new, derived FnPoint.
If you want (for some reason) to get a concrete `Point` instance, call `fp.concrete(loop: bool = False)`
Obviously, this will raise an error on an infinite FnPoint, so either pass a length into the constructor or as a slice:
```python
fp = FnPoint(lambda x: x * 2) # infinite point
fp_fin = FnPoint(lambda x: x * 2, length=4) # finite point
fp_slice = fp[:4] # also finite
# okay
fp_fin.concrete()
fp_slice.concrete()
# error
fp.concrete()
```
## Matrix
Now you can wake up and take a non-pointy pill, at last.
Matrices are N-dimensional tables, well, you can [read Wikipedia](https://en.wikipedia.org/wiki/Matrix_(mathematics)) instead of this.
In `easypoint`, matrices are quite straightforward (keep in mind, they have 0-based indexing):
```python
from easypoint import Matrix
mul_table = Matrix((10, 10))
for (y, x) in mul_table:
mul_table[y, x] = (y + 1) * x
from pprint import pprint
"""
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
...,
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
"""
pprint(mul_table.to_list())
```
As you can see, one can easily iterate over every index in matrix using iterator protocol. If you want to iterate over some portion of a matrix, use `matrix.iter()` explicitly:
```python
# Matrix.iter(self, start: Index | None = None, stop: Index | None = None):
mul_table = Matrix((10, 10))
for index in mul_table.iter((3, 3), (4, 5)):
mul_table[index] = 0 # why? idk
```
### Operations
As with `Point`, with matrices you can get an element-wise sum, difference and multiply matrix by a number.
Multiplication (as well as `@`) is reserved for matrix multiplication (or, generally, tensor contraction).
If you need an element-wise multiplication (or any other operation), you can use `Matrix.apply_bin`:
```python
from easypoint import Matrix
x_table = Matrix((10, 10))
y_table = x_table.new() # creates a new matrix of the same size
for (y, x) in x_table:
x_table[y, x] = (x + 1)
y_table[y, x] = (y + 1)
# Matrix.apply_bin(self, other: Matrix, func: Callable[[float, float], float], op: str = "?")
# `op` param is optional, it is an arbitrary string used for better debug
mul_table = x_table.apply_bin(y_table, lambda x, y: x * y, op="*")
from pprint import pprint
"""
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
...,
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]
"""
pprint(mul_table.to_list())
```
You can also apply a function to a single matrix with `apply`:
```python
coord_table = Matrix((10, 10))
for (y, x) in coord_table:
coord_table[y, x] = (x + y)
for (y, x) in coord_table:
coord_table.apply(lambda x: -x) # same as coord_table * -1
```
Other methods defined:
- `matrix.new()` creates an empty matrix of the same size (same as `Matrix(matrix.size)`),
- `matrix.copy()` copies the matrix (same as `matrix.apply(x: x)`)
- `matrix.transpose()` transposes the matrix (wow!)
- `matrix.cut(index)` returns a new matrix where all the rows/columns/etc. that pass through a specific index are removed.
- `matrix.get_submatrix(i: int)` for an N-dimensional matrix, returns an (N-1)-dimensional matrix at some index `i`. For example, used on a 2D matrix, returns an `i`-th row.
- `matrix.as_matrix(*points: PointLike)` builds a 2D matrix out of Point-like values.
### Internal state
Matrices in `easypoint` are implemented as flat dictionaries, with empty (default) values are omitted.
This helps with memory and speed if you have sparse matrices.
```python
matrix = Matrix((99999999, 99999999))
matrix[32474, 2387] # 0
matrix.data # {}
matrix[32474, 2387] = 327
matrix[32474, 2387] # 327
matrix.data # {3247399969913: 327}
```
If you need to swap the storage for something more efficient, build your own class.
For example, here's an example of possible read-only `FnMatrix` class:
```python
from easypoint import Matrix
from easypoint.internal_types import Size, Index, MatrixIndexFunc
class FnMatrix(Matrix):
def __init__(self, size: Size, fn: MatrixIndexFunc):
self.fn = fn
self.size = size
def get_index(self, index: Index):
return self.fn(index)
def set_index(self, index: Index, value: float):
raise ValueError("this matrix is read-only")
def copy(self):
return FnMatrix(self.size, self.fn)
def new(self):
return self.copy()
def sum_func(index: Index):
x, y = index
return x + y
fnm = FnMatrix(sum_func)
```
# TODO
- Better docs?
- Proper conversion from list to Matrix (although it's fairly easy now)
- Better test coverage
Raw data
{
"_id": null,
"home_page": "",
"name": "easypoint",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7,<4.0",
"maintainer_email": "",
"keywords": "",
"author": "Dmitry Gritsenko",
"author_email": "k01419q45@ya.ru",
"download_url": "https://files.pythonhosted.org/packages/3d/14/28442ac3fdbfabbe40845de3503c37a4c835505046c5a3812f7dc6aecf02/easypoint-0.2.0.tar.gz",
"platform": null,
"description": "# Minimal general-purpose vector / matrix arithmetics library\n\n# Installation\n\nInstall `easypoint` module:\n\n```bash\npoetry add easypoint\n```\n\nor\n\n```bash\npython -m pip install easypoint\n```\n\n\n# Introduction\n\neasypoint has 2 main types to work with: `Point` (a.k.a. `Vector`) and `Matrix`\n\n`Point` class builds up on my previous work with [evtn/soda](https://github.com/evtn/soda) and [evtn/soda-old](https://github.com/evtn/soda-old). \nBoth being graphics-oriented, so vector arithmetics is a must-have. \n\nBut over time, `Point` became a convenient class for various non-graphical tasks and tasks out of scope for `soda` (e.g. raster graphics). \nThis module also brings a refined `Matrix` class I've been using in various private/unfinished projects (an old version can be seen [here](https://gist.github.com/evtn/8683e58770f2901527275d46465e4cbe))\n\nBoth are refined and generalized for N dimensions. Some new additions (like `Point.transform(matrix: Matrix)`) are also in place.\n\n# Usage\n\n## Point\n\nPoint/Vector (`easypoint.Vector` is just an alias) is a fancy `Sequence[float]`, supporting various convenient operations.\n\n### Make a point\n\n```python\nfrom easypoint import Point\n\n# create a Point with numbers:\n\np1 = Point(1, 2, 3) # Point[1, 2, 3]\n\n# ...or from a list/tuple\n\np2 = Point.from_([1, 2, 3]) # Point[1, 2, 3]\n\n# ...from a number\n\np3 = Point.from_(1) # Point[1, ...]\n\n# ...from another Point\n\np4 = p1[:2] # Point[1, 2]\np5 = p1[0, 2, 1] # Point[1, 3, 2] \np6 = p3[:] # Error (a slice of an infinite Point)\n```\n\n```python\na = Point(1, 2)\nb = Point(4, 5)\n```\n\nIn any context where a point could be used, \"point-like\" values can be used:\n\n- `(1, 2)` <-> `Point(1, 2)`\n- `[1, 2]` <-> `Point(1, 2)`\n- `1` <-> `Point(1, loop=True)`\n\n### Math\n\nYou can perform mathematical operations on points (element-wise):\n\n```python\na + b # Point[5, 7]\na - b # Point[-3, -3]\na * b # Point[4, 10]\na / b # Point[0.25, 0.4]\na % b # Point[1, 2]\n```\n\n...and any point-like values:\n\n```python\na + 10 # Point[11, 12]\na * 2 # Point[2, 4]\n```\n\n### Distance\n\nYou also can calculate distance between points and get a normalized vector:\n\n```python\nfrom math import pi\n\na.distance(b) # 4.242640687119285\na.distance() # 2.23606797749979 (distance between a and (0, 0), basically the length of a vector)\na.normalized() # Point[0.4472135954999579, 0.8944271909999159]\n```\n\n### Rotation\n\n2D Rotation can be done around some center:\n\n```python\na.rotate2d(degrees=90) # Point[-2, 1]\na.rotate2d(center=(10, 10), radians=pi / 2) # Point[18, 1]\na.rotate2d(center=10, degrees=90) # Point[18, 1]\n\n# if you want to use axis other than (0, 1), pass `axis`:\nc = Point(4, 6, 2, 3, 2)\nc.rotate2d(center=10, radians=pi / 2, axis=(3, 4)) # Point[4, 6, 2, 18, 3]\n```\n\n### Transforms\n\nYou can transform an N-dimensional `Point` with a NxN `Matrix`:\n\n```python\nfrom easypoint import Point, Matrix\n# Shearing\n# 1 k\n# 0 1\n\nmatrix = Matrix.as_matrix((1, 4), (0, 1))\nt = Point(0, 5)\nt.transform(matrix) # Point[20, 5]\n```\n\n### Looped points\n\nSometimes it's convenient to have a point with `p[i] == p[i % n]` (a repeating set of coordinates). \nIt can be achieved by passing `loop=True` into `Point` constructor or `Point.from_`:\n\n```python\np1 = Point(10.3, loop=True) # Point[10.3, ...]\np1[54378] # 10.3\n\np2 = Point.from_([1, 2, 3], loop=True) # Point[1, 2, 3, ...]\np2[540:550] # Point[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]\n```\n\nKeep in mind that `Point.from_(int)` always produces a looped point, if you need a 1-dimensional point, use `Point(int)`\n\n### Indexing\n\nPoints support three types of indexing:\n\n- `point[int]` returns a value at that index, or 0 if this index doesn't exist (and the point is not looped)\n- `point[slice]` returns a `Point` with values under that slice\n- `point[tuple[int, ...]]` returns a Point with values under indices in the tuple\n\n```python\na = Point(*range(5)) # Point[0, 1, 2, 3, 4]\na[2] # 2\na[2:4] # Point[2, 3, 4]\na[4, 3, 8, 2] # Point[4, 3, 0, 2]\n```\n\nSame applies for setting values on indices. \nKeep in mind that setting a slice/tuple doesn't change the dimension count, extra indices/values are ignored\n\nThere are also `x`, `y`, and `z` properties as aliases for `[0]`, `[1]`, and `[2]`\n\n### Interpolation\n\nFor convenience, there are `point.interpolate(other: PointLike, k: float)` to interpolate between two points (self at 0, other at 1). \n`point.center(other: PointLike)` is an alias for `point.interpolate(other, 0.5)`\n\n### Naming\n\nYou can give any point a name (any string) for convenience and better output:\n\n```python\na = Point(3, 4) # Point[3, 4]\nb = a.named(\"B\") # Point<B>[3, 4]\n```\n\nNaming returns a copy of the point, so the original one is not renamed\n\n\n### FnPoint\n\nFnPoint is a Point defined by index function:\n\n```python\nfrom easypoint import FnPoint\n\nfp = FnPoint(lambda i: 126 * i * i + 7)\nfp[4] # 2023\n```\n\n...and optional length:\n\n```python\nfrom easypoint import FnPoint\n\nfp = FnPoint(lambda i: 126 * i * i + 7, length=3)\nfp[4] # 0\n\n```\n\nIt is fully compatible with Point, but any operation on FnPoint will return you a new, derived FnPoint. \n\nIf you want (for some reason) to get a concrete `Point` instance, call `fp.concrete(loop: bool = False)` \nObviously, this will raise an error on an infinite FnPoint, so either pass a length into the constructor or as a slice:\n\n```python\n\nfp = FnPoint(lambda x: x * 2) # infinite point\nfp_fin = FnPoint(lambda x: x * 2, length=4) # finite point\nfp_slice = fp[:4] # also finite\n\n# okay\nfp_fin.concrete()\nfp_slice.concrete()\n\n# error\nfp.concrete() \n\n```\n\n## Matrix\n\nNow you can wake up and take a non-pointy pill, at last. \n\nMatrices are N-dimensional tables, well, you can [read Wikipedia](https://en.wikipedia.org/wiki/Matrix_(mathematics)) instead of this.\n\nIn `easypoint`, matrices are quite straightforward (keep in mind, they have 0-based indexing):\n\n```python\nfrom easypoint import Matrix\n\n\nmul_table = Matrix((10, 10))\n\nfor (y, x) in mul_table:\n mul_table[y, x] = (y + 1) * x\n\nfrom pprint import pprint\n\n\"\"\"\n[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n ...,\n [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]\n\"\"\"\npprint(mul_table.to_list())\n```\n\nAs you can see, one can easily iterate over every index in matrix using iterator protocol. If you want to iterate over some portion of a matrix, use `matrix.iter()` explicitly:\n\n```python\n# Matrix.iter(self, start: Index | None = None, stop: Index | None = None):\n\nmul_table = Matrix((10, 10))\n\nfor index in mul_table.iter((3, 3), (4, 5)):\n mul_table[index] = 0 # why? idk\n\n```\n\n### Operations\n\nAs with `Point`, with matrices you can get an element-wise sum, difference and multiply matrix by a number. \nMultiplication (as well as `@`) is reserved for matrix multiplication (or, generally, tensor contraction). \nIf you need an element-wise multiplication (or any other operation), you can use `Matrix.apply_bin`:\n\n```python\nfrom easypoint import Matrix\n\nx_table = Matrix((10, 10))\ny_table = x_table.new() # creates a new matrix of the same size\n\nfor (y, x) in x_table:\n x_table[y, x] = (x + 1)\n y_table[y, x] = (y + 1)\n\n# Matrix.apply_bin(self, other: Matrix, func: Callable[[float, float], float], op: str = \"?\")\n# `op` param is optional, it is an arbitrary string used for better debug\nmul_table = x_table.apply_bin(y_table, lambda x, y: x * y, op=\"*\")\n\nfrom pprint import pprint\n\n\"\"\"\n[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n ...,\n [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]]\n\"\"\"\npprint(mul_table.to_list())\n\n```\n\nYou can also apply a function to a single matrix with `apply`:\n\n```python\ncoord_table = Matrix((10, 10))\n\nfor (y, x) in coord_table:\n coord_table[y, x] = (x + y)\n\nfor (y, x) in coord_table:\n coord_table.apply(lambda x: -x) # same as coord_table * -1\n\n```\n\nOther methods defined:\n\n- `matrix.new()` creates an empty matrix of the same size (same as `Matrix(matrix.size)`),\n- `matrix.copy()` copies the matrix (same as `matrix.apply(x: x)`)\n- `matrix.transpose()` transposes the matrix (wow!)\n- `matrix.cut(index)` returns a new matrix where all the rows/columns/etc. that pass through a specific index are removed.\n- `matrix.get_submatrix(i: int)` for an N-dimensional matrix, returns an (N-1)-dimensional matrix at some index `i`. For example, used on a 2D matrix, returns an `i`-th row.\n- `matrix.as_matrix(*points: PointLike)` builds a 2D matrix out of Point-like values.\n\n### Internal state\n\nMatrices in `easypoint` are implemented as flat dictionaries, with empty (default) values are omitted. \nThis helps with memory and speed if you have sparse matrices.\n\n```python\nmatrix = Matrix((99999999, 99999999))\n\nmatrix[32474, 2387] # 0\nmatrix.data # {}\n\nmatrix[32474, 2387] = 327\nmatrix[32474, 2387] # 327\nmatrix.data # {3247399969913: 327}\n```\n\nIf you need to swap the storage for something more efficient, build your own class. \nFor example, here's an example of possible read-only `FnMatrix` class:\n\n\n```python\nfrom easypoint import Matrix\nfrom easypoint.internal_types import Size, Index, MatrixIndexFunc\n\n\nclass FnMatrix(Matrix):\n def __init__(self, size: Size, fn: MatrixIndexFunc):\n self.fn = fn\n self.size = size\n \n def get_index(self, index: Index):\n return self.fn(index)\n \n def set_index(self, index: Index, value: float):\n raise ValueError(\"this matrix is read-only\")\n \n def copy(self):\n return FnMatrix(self.size, self.fn)\n \n def new(self):\n return self.copy()\n \n\ndef sum_func(index: Index):\n x, y = index\n return x + y \n\n\nfnm = FnMatrix(sum_func)\n```\n\n\n# TODO\n\n- Better docs?\n- Proper conversion from list to Matrix (although it's fairly easy now)\n- Better test coverage",
"bugtrack_url": null,
"license": "MIT",
"summary": "Minimal general-purpose vector / matrix arithmetics library",
"version": "0.2.0",
"project_urls": null,
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "7a735485971c8f5a2a17ff6e4c9edf0bfd7fa459178dc90b061123f77f3ae9e5",
"md5": "be6b08d849ee9f00f28551f7404abff7",
"sha256": "f64d2fe55300a9d90294cd53631e801748a7cd46ae08affaa102ba7bb949559d"
},
"downloads": -1,
"filename": "easypoint-0.2.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "be6b08d849ee9f00f28551f7404abff7",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7,<4.0",
"size": 12294,
"upload_time": "2023-07-28T09:07:53",
"upload_time_iso_8601": "2023-07-28T09:07:53.372121Z",
"url": "https://files.pythonhosted.org/packages/7a/73/5485971c8f5a2a17ff6e4c9edf0bfd7fa459178dc90b061123f77f3ae9e5/easypoint-0.2.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "3d1428442ac3fdbfabbe40845de3503c37a4c835505046c5a3812f7dc6aecf02",
"md5": "48fb06e743639a4da9df1532958436c6",
"sha256": "dcf3e292504f5e26208eb7d61efdc75dcf6e6c18e3703e2c97e2b569cfc982a4"
},
"downloads": -1,
"filename": "easypoint-0.2.0.tar.gz",
"has_sig": false,
"md5_digest": "48fb06e743639a4da9df1532958436c6",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7,<4.0",
"size": 13794,
"upload_time": "2023-07-28T09:07:54",
"upload_time_iso_8601": "2023-07-28T09:07:54.877744Z",
"url": "https://files.pythonhosted.org/packages/3d/14/28442ac3fdbfabbe40845de3503c37a4c835505046c5a3812f7dc6aecf02/easypoint-0.2.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-07-28 09:07:54",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "easypoint"
}