rockhopper


Namerockhopper JSON
Version 0.2.0 PyPI version JSON
download
home_pagehttps://github.com/bwoodsend/rockhopper
SummaryRagged (rows with different lengths) 2D NumPy arrays.
upload_time2022-06-30 22:17:26
maintainer
docs_urlNone
authorBrénainn Woodsend
requires_python>=3.6
licenseMIT license
keywords rockhopper
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            ==========
rockhopper
==========

.. image::
    https://img.shields.io/badge/
    Python-%203.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%20PyInstaller-blue.svg

A *Ragged Array* class: 2D NumPy arrays containing rows of mismatching lengths.

* Free software: MIT license
* Source code: https://github.com/bwoodsend/rockhopper/
* Releases: https://pypi.org/project/rockhopper/
* Documentation: You are looking at it... 🤨

NumPy arrays are very powerful but its multidimensional arrays must be
rectangular (or cuboidal, hyper-cuboidal, tesseractal, ...).
A ``rockhopper.RaggedArray()`` wraps a 1D NumPy array into something resembling
a 2D NumPy array but with the *rectangular* constraint loosened.
i.e. The following is perfectly valid:

.. code-block:: python

    from rockhopper import ragged_array

    ragged = ragged_array([
        # Row with 4 items
        [1.2, 23.3, 4.1 , 12],
        # Row with 3 items
        [2.0, 3., 43.9],
        # Row with no items
        [],
        # Another row with 4 items
        [0.12, 7.2, 1.3, 42.9],
    ])

Under the hood,
rockhopper operations use NumPy vectorisation where possible
and C when not
so that performance is almost as good as normal NumPy
and still orders of magnitudes faster than pure Python list of lists
implementations.


Features
--------

It's early days for **rockhopper**.
Features have so far been added on an *as needed* basis
and consequently, its features list has some holes in it.
The following shows what **rockhopper** has, labelled with a ✓,
and what it doesn't (yet) have, labelled with a ✗.

* `Initialisation`_ from:
    - ✓ A ragged list of lists.
    - ✓ A flat contents array and a list of row lengths.
    - ✓ A flat contents array and a list of row start/ends.
* `Indexing and Slicing`_ (getting/setting support marked separately with a ``'/'`` divider):
    - `1D indices`_ ``ragged[rows]`` where:
        * ✓/✓: **rows** is an integer.
        * ✓/✗: **rows** is a list of integers, bool mask or slice.
    - `2D indices`_ ``ragged[rows, columns]`` where:
        * ✓/✓ **rows** is anything and **columns** is an integer or list of
          integers.
        * ✓/✗: **rows** is anything and **columns** is a bool mask or slice.
    - `3D (or higher) indices`_ ``ragged[x, y, z]`` (only applicable to `higher dimensional arrays`_) where:
        * ✓/✓ **x** is anything, **y** is an integer or list of integers, and
          **z** is anything.
        * ✗/✗: **x** is anything, and **y** is a bool mask or slice, and **z**
          is anything.
* Concatenation (joining multiple arrays together):
    - ✗ rows
    - ✗ columns
* Vectorisation - these will take a bit of head scratching to get working:
    - ✗ Applying arithmetic operations (e.g. ``ragged_array * 3``) so that the
      for loop is efficiently handled in NumPy.
    - ✗ Reverse ``__getitem__()``. i.e. ``regular_array[ragged_integer_array]``
      should create another ragged array whose contents are taken from
      ``regular_array``.
* `Export to standard types`_:
    - ✓ The ``tolist()`` method takes you back to a list of lists.
    - ✓ The ``to_rectangular_arrays()`` method converts to a list of regular
      rectangular arrays.
* `Serialisation and deserialisation`_:
    - ✓ Binary_ (``row-length|row-content`` format).
    - ✗ Ascii. (Saving this for a rainy day.)
    - ✓ Pickle_.
* ✓ Grouping_ data by some enumeration - similar to
  ``pandas.DataFrame.groupby()``.


Installation
------------

To install use the following steps:

1.  Think of a prime number between 4294967296 and 18446744073709551616,
2.  Multiply it by the diameter of your ear lobes,
3.  Negate it then take the square root,
4.  Subtract the number you first thought of,
5.  Run the following in some flavour of terminal::

        pip install rockhopper

Pre-built binary wheels (i.e. easy to install) are shipped for:

* Linux distributions based on glibc whose architecture NumPy also ships
  prebuilt wheels for (which can be seen `here
  <https://pypi.org/project/numpy/#files>`_)
* Windows 64 and 32 bit
* macOS >=10.6 on ``x86_86`` or ``arm64``

Other supported and tested platforms (which ``wheel`` lacks support for) are:

* musl based Linux (requires gcc_ to build)
* FreeBSD (requires clang_ or gcc_ to build)

On these platforms, **rockhopper** should build from and install out the box
if your first install the appropriate C compiler.

.. _many linux project: https://quay.io/organization/pypa
.. _gcc: https://gcc.gnu.org/
.. _clang: https://clang.llvm.org/


Usage
-----


Initialisation
..............

The easiest way to make a ragged array is from a nested list using
``rockhopper.ragged_array()``.

.. code-block:: python

    from rockhopper import ragged_array

    ragged = ragged_array([
        [1, 2, 3],
        [2, 43],
        [34, 32, 12],
        [2, 3],
    ])

In this form, what goes in is what comes out.

.. code-block:: python

    >>> ragged
    RaggedArray.from_nested([
        [1, 2, 3],
        [ 2, 43],
        [34, 32, 12],
        [2, 3],
    ])

As the repr implies, the output is of type ``rockhopper.RaggedArray`` and
the ``ragged_array()`` function is simply a shortcut for
``RaggedArray.from_nested()`` which you may call directly if you prefer.
Data types (the `numpy.dtype`_) are implicit but may be overrode using the
**dtype** parameter.


.. code-block:: python

    >>> ragged_array([
    ...     [1, 2, 3],
    ...     [2, 43],
    ...     [34, 32, 12],
    ...     [2, 3],
    ... ], dtype=float)
    RaggedArray.from_nested([
        [1., 2., 3.],
        [ 2., 43.],
        [34., 32., 12.],
        [2., 3.],
    ])


.. _`numpy.dtype`: https://numpy.org/doc/stable/reference/arrays.dtypes.html

Alternative ways to construct are from flat contents and row lengths:

.. code-block:: python

    from rockhopper import RaggedArray

    # Creates exactly the same array as above.
    ragged = RaggedArray.from_lengths(
        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents.
        [3, 2, 3, 2],  # The length of each row.
    )

Or at a lower level, a flat contents array and an array of row *bounds* (the
indices at which one row ends and next one begins).
As with regular Python ``range()`` and slices, a row includes the starting index
but excludes the end index.

.. code-block:: python

    # Creates exactly the same array as above.
    ragged = RaggedArray(
        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents again.
        [0, 3, 5, 8, 10],  # The start and end of each row.
    )

Or at an even lower level, a flat contents array and separate arrays for where
each row starts and each row ends.
This form reflects how the ``RaggedArray`` class's internals are structured.

.. code-block:: python

    # And creates the same array as above again.
    ragged = RaggedArray(
        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents.
        [0, 3, 5, 8],  # The starting index of each row.
        [3, 5, 8, 10],  # The ending index of each row.
    )

This last form is used internally for efficient slicing but isn't expected to be
particularly useful for day to day usage.
With this form, rows may be in mixed orders, have gaps between them or overlap.

.. code-block:: python

    # Creates a weird array.
    ragged = RaggedArray(
        range(10),  # The array contents.
        [6, 3, 4, 1, 2],  # The starting index of each row.
        [9, 5, 8, 2, 2],  # The ending index of each row.
    )

Externally, the fact that rows share data or have gaps in between is invisible.

.. code-block:: python

    >>> ragged
    RaggedArray.from_nested([
        [6, 7, 8],
        [3, 4],
        [4, 5, 6, 7],
        [1],
        [],
    ])


Higher Dimensional Arrays
*************************

Rockhopper is very much geared towards 2D ragged arrays, however,
one permutation of higher dimensional ragged arrays is allowed:
A ragged array's rows can be multidimensional rather than a 1D arrays.

Construction works more or less as you'd expect.
The following shows 3 different ways to create the same multidimensional ragged
array.

.. code-block:: python

    import numpy as np
    from rockhopper import ragged_array, RaggedArray

    # Construct from nested lists.
    from_nested = ragged_array([
        [[0,  1], [2, 3]],
        [[4, 5]],
        [[6, 7], [8, 9], [10, 11]],
        [[12, 13]],
    ])

    # Construction from flat contents and either ...
    flat = np.array([
        [0,  1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13]
    ])
    # ... row lengths, ...
    from_lengths = RaggedArray.from_lengths(flat, [2, 1, 3, 2])
    # ... or row bounds.
    from_bounds = RaggedArray(flat, [0, 2, 3, 6, 7])


Structured Arrays
*****************

Ragged arrays may also use a `structured data type
<https://numpy.org/doc/stable/user/basics.rec.html>`_.
For this, explicitly setting the **dtype** parameter is mandatory when using
the ``ragged_array()`` constructor.
Otherwise NumPy will cast everything to one compatible type (usually ``str``).

.. code-block:: python

    ragged = ragged_array([
        [("abc", 3), ("efg", 5)],
        [("hij", 1)],
        [("klm", 13), ("nop", 99), ("qrs", 32)],
    ], dtype=[("foo", str, 3), ("bar", int)])

However, this feature is only half-formed because ``ragged["foo"]`` requires
internal support for strided flat arrays (which rockhopper currently lacks).


Indexing and Slicing
....................

Most forms of ``__getitem__()`` and ``__setitem__()``
(i.e. ``ragged[x]`` and ``ragged[x] = y``)
are supported and mirror the semantics of `NumPy indexing`_.

There are a few general rules of thumb for what isn't supported:

* When a get operation returns another ragged array, the corresponding set
  operation is not implemented. This would require implementing vectorisation to
  work.
* If a 2D index ``ragged[x, y]`` gives another ragged array, then neither
  getting or setting is supported for >2D indices which start with said 2D index
  ``ragged[x, y, z]``. This would require internal support for letting
  ``ragged.flat`` be strided.
* Ragged arrays can not be used as indices. ``arr[ragged]`` will fail
  irregardless or whether ``arr`` is ragged or not.
* Under no circumstances will writing to a ragged array be allowed to change
  its overall length or the length of one of its rows.

In all cases except where indicated otherwise,
indexing returns original data - not copies.
If you later write to either the ragged array itself or a slice taken from it,
then the other will change too.

.. _NumPy indexing: https://numpy.org/doc/stable/reference/arrays.indexing.html


1D indices
**********

Indexing will all be shown by examples.
Here is an unimaginative ragged array to play with.

.. code-block:: python

    from rockhopper import ragged_array

    ragged = ragged_array([
        [1, 2, 3, 4],
        [5, 6],
        [7, 8, 9],
        [10, 11, 12, 13],
    ])

1D indexing with individual integers gives single rows as regular arrays.

.. code-block:: python

    >>> ragged[2]
    array([7, 8, 9])
    >>> ragged[3]
    array([10, 11, 12, 13])

But indexing with a slice, integer array or bool mask gives another ragged
array.

.. code-block:: python

    >>> ragged[::2]
    RaggedArray.from_nested([
        [1, 2, 3, 4],
        [7, 8, 9],
    ])
    >>> ragged[[2, -1]]
    RaggedArray.from_nested([
        [7, 8, 9],
        [10, 11, 12, 13],
    ])


This is true even if all rows happen to be the same length.


2D indices
**********

2D indexing ``ragged[rows, columns]`` gives individual cells.
Arrays of indices, slices and bool masks may also be used instead of single
numbers.
Using the same boring ragged array `as above <#d-indices>`_:

.. code-block:: python

    # Individual indices.
    >>> ragged[0, 0], ragged[0, 1], ragged[0, 2]
    (1, 2, 3)

    # Arrays of indices.
    >>> ragged[0, [0, 1, -1]]
    array([1, 2, 4])
    >>> ragged[0, [[1, 2], [0, 2]]]
    array([[2, 3],
           [1, 3]])
    >>> ragged[[0, 3, 2], [2, 3, 1]]
    array([ 3, 13,  8])

    # Slices as row numbers (including the null slice [:]).
    >>> ragged[:, 0]
    array([ 1,  5,  7, 10])
    >>> ragged[2:, -1]
    array([ 9, 13])

    # Again, multiple column numbers may be given.
    # The following gets the first and last element from each row.
    >>> ragged[:, [0, -1]]
    array([[ 1,  4],
           [ 5,  6],
           [ 7,  9],
           [10, 13]])

    # If the second index is a slice or bool mask, the output is a ragged array.
    # Even if each row is of the same length.
    >>> ragged[:, :2]
    RaggedArray.from_nested([
        [1, 2],
        [5, 6],
        [7, 8],
        [10, 11],
    ])

If the second index is not a slice then the the output of getitem is a copy and
does not share memory with the parent ragged array.


3D (or higher) indices
**********************

`Higher Dimensional Arrays`_ can be sliced using 3 indices (or more).

Using another uninspiring enumeration example - this time a 3D array:

.. code-block:: python

    ragged = ragged_array([
        [[ 0,  1,  2], [ 3,  4,  5]],
        [[ 6,  7,  8], [ 9, 10, 11]],
        [[12, 13, 14], [15, 16, 17], [18, 19, 20]],
        [[21, 22, 23]],
    ])

3D arrays follow the same indexing rules as 2D arrays except that each **cell**
is actually another array.

.. code-block:: python

    >>> ragged[0, 1]
    array([3, 4, 5])

And a triplet of indices are used to access individual elements.

.. code-block:: python

    >>> ragged[2, 0, 1]
    13


Export to standard types
........................

No matter how many features I cram in to make ragged arrays more interchangeable
with normal ones,
you'll probably want to get back into regular array territory at the first
opportunity.
**rockhopper** comes with a few ways to do so.

First, let us create a ragged array to export:

.. code-block:: python

    from rockhopper import ragged_array
    ragged = ragged_array([
        [1, 2, 3],
        [4, 5, 6],
        [7, 8],
        [9, 10],
        [11, 12, 13],
    ])


To list of lists
****************

The ``tolist()`` method converts back to nested lists (like those used to build
the array in the first place).

.. code-block:: python

    >>> ragged.tolist()
    [[1, 2, 3], [4, 5, 6], [7, 8], [9, 10], [11, 12, 13]]


To list of homogenous arrays
****************************

When a ragged array is either not very ragged (row lengths are mostly the same)
or not ragged at all (rows are all the same length),
it's often helpful to split it on rows of differing lengths,
giving a sequence of standard rectangular arrays which can be ``for loop``\ -ed
over.
Do this with the ``to_rectangular_arrays()`` method.

.. code-block:: python

    >>> ragged.to_rectangular_arrays()
    [array([[1, 2, 3],
            [4, 5, 6]]),
     array([[7, 8],
            [9, 10]]),
     array([[11, 12, 13]])]

In the somewhat unlikely event that you don't care about the order the rows
appear in,
set the **reorder** option to allow it to presort the rows into ascending
lengths so as to minimize fragmentation.

.. code-block:: python

    >>> sort_args, arrays = ragged.to_rectangular_arrays(reorder=True)
    # The numpy.argsort() arguments are returned in case you want them.
    >>> sort_args
    array([2, 3, 0, 1, 4])
    # By sorting, only 2 arrays are needed rather than 3.
    >>> arrays
    [array([[ 7,  8],
            [ 9, 10]]),
     array([[ 1,  2,  3],
            [ 4,  5,  6],
            [11, 12, 13]])]


Serialisation and Deserialisation
.................................

Ragged arrays may be converted to bytes and back again
which can be read from or written to files.


Binary
******

Currently **rockhopper** knows of exactly one binary format:
The highly typical, but hopelessly un-NumPy-friendly::

    row-length | row-content | row-length | row-content

binary form often found in 3D graphics
where ``row-length`` may be any unsigned integer type of either byteorder,
``row-content`` may be of any data type or byteorder,
and there are no delimiters or metadata anywhere.

For this format ``RaggedArray()`` provides a ``loads()`` method for reading
and a ``dumps()`` method for writing.

Some examples:

.. code-block:: python

    # Write using:
    #  - Row contents: The current data type (ragged.dtype) and endian.
    #  - Row lengths: ``numpy.intc`` native endian
    # Note that the output is a memoryview() which is generally interchangeable
    # with bytes(). This may still be written to a file with the usual
    # ``fh.write()``.
    dumped = ragged.dumps()

    # Read back using:
    #  - Row contents: The same dtype used to write it
    #  - Row lengths: ``numpy.intc`` native endian
    ragged, bytes_consumed = RaggedArray.loads(dumped, ragged.dtype)

    # Write then read using:
    #  - Row contents: Big endian 8-byte floats
    #  - Row lengths: Little endian 2-byte unsigned integers
    dumped = ragged.astype(">f8").dumps(ldtype="<u2")
    ragged, bytes_consumed = RaggedArray.loads(dumped, ">f8", ldtype="<u2")

By default, ``loads()`` will keep adding rows until it hits the end of the byte
array that it's parsing.
The ``bytes_consumed`` (a count of how many bytes from ``dumped`` where used)
will therefore always satisfy ``bytes_consumed == len(dumped)``.

Some file formats contain a serialised ragged array embedded inside a larger
file but don't specify how many bytes belong to
the ragged array and how many belong to whatever comes afterwards.
Instead they specify how many rows there should be.
To read such data use the **rows** keyword argument.

.. code-block:: python

    # Read a 20 row ragged array of floats from a long ``bytes()`` object called
    # **blob**. Will raise an error if it runs out of data.
    ragged, bytes_consumed = ragged.loads(blob, "f8", rows=20)

    # ``bytes_consumed`` indicates where the ragged array stopped.
    rest_of_blob = blob[bytes_consumed:]


Pickle
******

If you don't need other programs to be able to read the output then bog-standard
pickle works too.

.. code-block:: python

    >>> import pickle
    >>> arr = ragged_array([
    ...    ["cake", "biscuits"],
    ...    ["socks"],
    ...    ["orange", "lemon", "pineapple"],
    ... ])
    >>> pickle.loads(pickle.dumps(arr))
    RaggedArray.from_nested([
        ["cake", "biscuits"],
        ["socks"],
        ["orange", "lemon", "pineapple"],
    ])


Grouping
........

Arbitrary data may be grouped by some group enumeration into a ragged array so
that each data element appears on the row of its group number.

For example, to group the people in the following array...

.. code-block:: python

    people = np.array([
        ("Bob", 1),
        ("Bill", 2),
        ("Ben", 0),
        ("Biff", 1),
        ("Barnebas", 0),
        ("Bubulous", 1),
        ("Bofflodor", 2),
    ], dtype=[("name", str, 20), ("group number", int)])

... by the **group number** field use:

.. code-block:: python

    >>> from rockhopper import RaggedArray
    >>> RaggedArray.group_by(people, people["group number"])
    RaggedArray.from_nested([
        [('Ben', 0), ('Barnebas', 0)],
        [('Bob', 1), ('Biff', 1), ('Bubulous', 1)],
        [('Bill', 2), ('Bofflodor', 2)],
    ])

As you can hopefully see,

- all the names given a **group number** 0 appear in row 0,
- all the names given a **group number** 1 appear in row 1,
- and all the names given a **group number** 1 appear in row 2.

At this point you probably no longer care about the **group number** field,
in which case, group only the **name** field:

.. code-block:: python

    >>> RaggedArray.group_by(people["name"], people["group number"])
    RaggedArray.from_nested([
        ['Ben', 'Barnebas'],
        ['Bob', 'Biff', 'Bibulous'],
        ['Bill', 'Bofflodor'],
    ])


Enumerating classes
*******************

The above assumes that the parameter you wish to group by is just an
enumeration.
If this is not the case, and you're not already sick of software written by me,
then you may use a `hirola.HashTable()
<https://github.com/bwoodsend/Hirola#hirola>`_ to efficiently enumerate the
parameter to group by.

For example, to group this list of animals by their animal class:

.. code-block:: python

    animals = np.array([
        ("cow", "mammal"),
        ("moose", "mammal"),
        ("centipede", "insect"),
        ("robin", "bird"),
        ("spider", "insect"),
        ("whale", "mammal"),
        ("woodpecker", "bird"),
    ], dtype=[("name", str, 15), ("class", str, 15)])

Use something like:

.. code-block:: python

    >>> from hirola import HashTable
    >>> animal_classes = HashTable(len(animals), animals.dtype["class"])
    >>> enum = animal_classes.add(animals["class"])

    >>> RaggedArray.group_by(animals["name"], enum)
    RaggedArray.from_nested([
        ['cow', 'moose', 'whale'],
        ['centipede', 'spider'],
        ['robin', 'woodpecker'],
    ])
    >>> animal_classes.keys
    array(['mammal', 'insect', 'bird'], dtype='<U15')

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/bwoodsend/rockhopper",
    "name": "rockhopper",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.6",
    "maintainer_email": "",
    "keywords": "rockhopper",
    "author": "Br\u00e9nainn Woodsend",
    "author_email": "bwoodsend@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/8d/a0/5dc6a9d367dd08b22dd3783afc54aa222dfde87b45a516504f62f19c2566/rockhopper-0.2.0.tar.gz",
    "platform": null,
    "description": "==========\nrockhopper\n==========\n\n.. image::\n    https://img.shields.io/badge/\n    Python-%203.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%20PyInstaller-blue.svg\n\nA *Ragged Array* class: 2D NumPy arrays containing rows of mismatching lengths.\n\n* Free software: MIT license\n* Source code: https://github.com/bwoodsend/rockhopper/\n* Releases: https://pypi.org/project/rockhopper/\n* Documentation: You are looking at it... \ud83e\udd28\n\nNumPy arrays are very powerful but its multidimensional arrays must be\nrectangular (or cuboidal, hyper-cuboidal, tesseractal, ...).\nA ``rockhopper.RaggedArray()`` wraps a 1D NumPy array into something resembling\na 2D NumPy array but with the *rectangular* constraint loosened.\ni.e. The following is perfectly valid:\n\n.. code-block:: python\n\n    from rockhopper import ragged_array\n\n    ragged = ragged_array([\n        # Row with 4 items\n        [1.2, 23.3, 4.1 , 12],\n        # Row with 3 items\n        [2.0, 3., 43.9],\n        # Row with no items\n        [],\n        # Another row with 4 items\n        [0.12, 7.2, 1.3, 42.9],\n    ])\n\nUnder the hood,\nrockhopper operations use NumPy vectorisation where possible\nand C when not\nso that performance is almost as good as normal NumPy\nand still orders of magnitudes faster than pure Python list of lists\nimplementations.\n\n\nFeatures\n--------\n\nIt's early days for **rockhopper**.\nFeatures have so far been added on an *as needed* basis\nand consequently, its features list has some holes in it.\nThe following shows what **rockhopper** has, labelled with a \u2713,\nand what it doesn't (yet) have, labelled with a \u2717.\n\n* `Initialisation`_ from:\n    - \u2713 A ragged list of lists.\n    - \u2713 A flat contents array and a list of row lengths.\n    - \u2713 A flat contents array and a list of row start/ends.\n* `Indexing and Slicing`_ (getting/setting support marked separately with a ``'/'`` divider):\n    - `1D indices`_ ``ragged[rows]`` where:\n        * \u2713/\u2713: **rows** is an integer.\n        * \u2713/\u2717: **rows** is a list of integers, bool mask or slice.\n    - `2D indices`_ ``ragged[rows, columns]`` where:\n        * \u2713/\u2713 **rows** is anything and **columns** is an integer or list of\n          integers.\n        * \u2713/\u2717: **rows** is anything and **columns** is a bool mask or slice.\n    - `3D (or higher) indices`_ ``ragged[x, y, z]`` (only applicable to `higher dimensional arrays`_) where:\n        * \u2713/\u2713 **x** is anything, **y** is an integer or list of integers, and\n          **z** is anything.\n        * \u2717/\u2717: **x** is anything, and **y** is a bool mask or slice, and **z**\n          is anything.\n* Concatenation (joining multiple arrays together):\n    - \u2717 rows\n    - \u2717 columns\n* Vectorisation - these will take a bit of head scratching to get working:\n    - \u2717 Applying arithmetic operations (e.g. ``ragged_array * 3``) so that the\n      for loop is efficiently handled in NumPy.\n    - \u2717 Reverse ``__getitem__()``. i.e. ``regular_array[ragged_integer_array]``\n      should create another ragged array whose contents are taken from\n      ``regular_array``.\n* `Export to standard types`_:\n    - \u2713 The ``tolist()`` method takes you back to a list of lists.\n    - \u2713 The ``to_rectangular_arrays()`` method converts to a list of regular\n      rectangular arrays.\n* `Serialisation and deserialisation`_:\n    - \u2713 Binary_ (``row-length|row-content`` format).\n    - \u2717 Ascii. (Saving this for a rainy day.)\n    - \u2713 Pickle_.\n* \u2713 Grouping_ data by some enumeration - similar to\n  ``pandas.DataFrame.groupby()``.\n\n\nInstallation\n------------\n\nTo install use the following steps:\n\n1.  Think of a prime number between 4294967296 and 18446744073709551616,\n2.  Multiply it by the diameter of your ear lobes,\n3.  Negate it then take the square root,\n4.  Subtract the number you first thought of,\n5.  Run the following in some flavour of terminal::\n\n        pip install rockhopper\n\nPre-built binary wheels (i.e. easy to install) are shipped for:\n\n* Linux distributions based on glibc whose architecture NumPy also ships\n  prebuilt wheels for (which can be seen `here\n  <https://pypi.org/project/numpy/#files>`_)\n* Windows 64 and 32 bit\n* macOS >=10.6 on ``x86_86`` or ``arm64``\n\nOther supported and tested platforms (which ``wheel`` lacks support for) are:\n\n* musl based Linux (requires gcc_ to build)\n* FreeBSD (requires clang_ or gcc_ to build)\n\nOn these platforms, **rockhopper** should build from and install out the box\nif your first install the appropriate C compiler.\n\n.. _many linux project: https://quay.io/organization/pypa\n.. _gcc: https://gcc.gnu.org/\n.. _clang: https://clang.llvm.org/\n\n\nUsage\n-----\n\n\nInitialisation\n..............\n\nThe easiest way to make a ragged array is from a nested list using\n``rockhopper.ragged_array()``.\n\n.. code-block:: python\n\n    from rockhopper import ragged_array\n\n    ragged = ragged_array([\n        [1, 2, 3],\n        [2, 43],\n        [34, 32, 12],\n        [2, 3],\n    ])\n\nIn this form, what goes in is what comes out.\n\n.. code-block:: python\n\n    >>> ragged\n    RaggedArray.from_nested([\n        [1, 2, 3],\n        [ 2, 43],\n        [34, 32, 12],\n        [2, 3],\n    ])\n\nAs the repr implies, the output is of type ``rockhopper.RaggedArray`` and\nthe ``ragged_array()`` function is simply a shortcut for\n``RaggedArray.from_nested()`` which you may call directly if you prefer.\nData types (the `numpy.dtype`_) are implicit but may be overrode using the\n**dtype** parameter.\n\n\n.. code-block:: python\n\n    >>> ragged_array([\n    ...     [1, 2, 3],\n    ...     [2, 43],\n    ...     [34, 32, 12],\n    ...     [2, 3],\n    ... ], dtype=float)\n    RaggedArray.from_nested([\n        [1., 2., 3.],\n        [ 2., 43.],\n        [34., 32., 12.],\n        [2., 3.],\n    ])\n\n\n.. _`numpy.dtype`: https://numpy.org/doc/stable/reference/arrays.dtypes.html\n\nAlternative ways to construct are from flat contents and row lengths:\n\n.. code-block:: python\n\n    from rockhopper import RaggedArray\n\n    # Creates exactly the same array as above.\n    ragged = RaggedArray.from_lengths(\n        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents.\n        [3, 2, 3, 2],  # The length of each row.\n    )\n\nOr at a lower level, a flat contents array and an array of row *bounds* (the\nindices at which one row ends and next one begins).\nAs with regular Python ``range()`` and slices, a row includes the starting index\nbut excludes the end index.\n\n.. code-block:: python\n\n    # Creates exactly the same array as above.\n    ragged = RaggedArray(\n        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents again.\n        [0, 3, 5, 8, 10],  # The start and end of each row.\n    )\n\nOr at an even lower level, a flat contents array and separate arrays for where\neach row starts and each row ends.\nThis form reflects how the ``RaggedArray`` class's internals are structured.\n\n.. code-block:: python\n\n    # And creates the same array as above again.\n    ragged = RaggedArray(\n        [1, 2, 3, 2, 43 34, 32, 12, 2, 3],  # The array contents.\n        [0, 3, 5, 8],  # The starting index of each row.\n        [3, 5, 8, 10],  # The ending index of each row.\n    )\n\nThis last form is used internally for efficient slicing but isn't expected to be\nparticularly useful for day to day usage.\nWith this form, rows may be in mixed orders, have gaps between them or overlap.\n\n.. code-block:: python\n\n    # Creates a weird array.\n    ragged = RaggedArray(\n        range(10),  # The array contents.\n        [6, 3, 4, 1, 2],  # The starting index of each row.\n        [9, 5, 8, 2, 2],  # The ending index of each row.\n    )\n\nExternally, the fact that rows share data or have gaps in between is invisible.\n\n.. code-block:: python\n\n    >>> ragged\n    RaggedArray.from_nested([\n        [6, 7, 8],\n        [3, 4],\n        [4, 5, 6, 7],\n        [1],\n        [],\n    ])\n\n\nHigher Dimensional Arrays\n*************************\n\nRockhopper is very much geared towards 2D ragged arrays, however,\none permutation of higher dimensional ragged arrays is allowed:\nA ragged array's rows can be multidimensional rather than a 1D arrays.\n\nConstruction works more or less as you'd expect.\nThe following shows 3 different ways to create the same multidimensional ragged\narray.\n\n.. code-block:: python\n\n    import numpy as np\n    from rockhopper import ragged_array, RaggedArray\n\n    # Construct from nested lists.\n    from_nested = ragged_array([\n        [[0,  1], [2, 3]],\n        [[4, 5]],\n        [[6, 7], [8, 9], [10, 11]],\n        [[12, 13]],\n    ])\n\n    # Construction from flat contents and either ...\n    flat = np.array([\n        [0,  1], [2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13]\n    ])\n    # ... row lengths, ...\n    from_lengths = RaggedArray.from_lengths(flat, [2, 1, 3, 2])\n    # ... or row bounds.\n    from_bounds = RaggedArray(flat, [0, 2, 3, 6, 7])\n\n\nStructured Arrays\n*****************\n\nRagged arrays may also use a `structured data type\n<https://numpy.org/doc/stable/user/basics.rec.html>`_.\nFor this, explicitly setting the **dtype** parameter is mandatory when using\nthe ``ragged_array()`` constructor.\nOtherwise NumPy will cast everything to one compatible type (usually ``str``).\n\n.. code-block:: python\n\n    ragged = ragged_array([\n        [(\"abc\", 3), (\"efg\", 5)],\n        [(\"hij\", 1)],\n        [(\"klm\", 13), (\"nop\", 99), (\"qrs\", 32)],\n    ], dtype=[(\"foo\", str, 3), (\"bar\", int)])\n\nHowever, this feature is only half-formed because ``ragged[\"foo\"]`` requires\ninternal support for strided flat arrays (which rockhopper currently lacks).\n\n\nIndexing and Slicing\n....................\n\nMost forms of ``__getitem__()`` and ``__setitem__()``\n(i.e. ``ragged[x]`` and ``ragged[x] = y``)\nare supported and mirror the semantics of `NumPy indexing`_.\n\nThere are a few general rules of thumb for what isn't supported:\n\n* When a get operation returns another ragged array, the corresponding set\n  operation is not implemented. This would require implementing vectorisation to\n  work.\n* If a 2D index ``ragged[x, y]`` gives another ragged array, then neither\n  getting or setting is supported for >2D indices which start with said 2D index\n  ``ragged[x, y, z]``. This would require internal support for letting\n  ``ragged.flat`` be strided.\n* Ragged arrays can not be used as indices. ``arr[ragged]`` will fail\n  irregardless or whether ``arr`` is ragged or not.\n* Under no circumstances will writing to a ragged array be allowed to change\n  its overall length or the length of one of its rows.\n\nIn all cases except where indicated otherwise,\nindexing returns original data - not copies.\nIf you later write to either the ragged array itself or a slice taken from it,\nthen the other will change too.\n\n.. _NumPy indexing: https://numpy.org/doc/stable/reference/arrays.indexing.html\n\n\n1D indices\n**********\n\nIndexing will all be shown by examples.\nHere is an unimaginative ragged array to play with.\n\n.. code-block:: python\n\n    from rockhopper import ragged_array\n\n    ragged = ragged_array([\n        [1, 2, 3, 4],\n        [5, 6],\n        [7, 8, 9],\n        [10, 11, 12, 13],\n    ])\n\n1D indexing with individual integers gives single rows as regular arrays.\n\n.. code-block:: python\n\n    >>> ragged[2]\n    array([7, 8, 9])\n    >>> ragged[3]\n    array([10, 11, 12, 13])\n\nBut indexing with a slice, integer array or bool mask gives another ragged\narray.\n\n.. code-block:: python\n\n    >>> ragged[::2]\n    RaggedArray.from_nested([\n        [1, 2, 3, 4],\n        [7, 8, 9],\n    ])\n    >>> ragged[[2, -1]]\n    RaggedArray.from_nested([\n        [7, 8, 9],\n        [10, 11, 12, 13],\n    ])\n\n\nThis is true even if all rows happen to be the same length.\n\n\n2D indices\n**********\n\n2D indexing ``ragged[rows, columns]`` gives individual cells.\nArrays of indices, slices and bool masks may also be used instead of single\nnumbers.\nUsing the same boring ragged array `as above <#d-indices>`_:\n\n.. code-block:: python\n\n    # Individual indices.\n    >>> ragged[0, 0], ragged[0, 1], ragged[0, 2]\n    (1, 2, 3)\n\n    # Arrays of indices.\n    >>> ragged[0, [0, 1, -1]]\n    array([1, 2, 4])\n    >>> ragged[0, [[1, 2], [0, 2]]]\n    array([[2, 3],\n           [1, 3]])\n    >>> ragged[[0, 3, 2], [2, 3, 1]]\n    array([ 3, 13,  8])\n\n    # Slices as row numbers (including the null slice [:]).\n    >>> ragged[:, 0]\n    array([ 1,  5,  7, 10])\n    >>> ragged[2:, -1]\n    array([ 9, 13])\n\n    # Again, multiple column numbers may be given.\n    # The following gets the first and last element from each row.\n    >>> ragged[:, [0, -1]]\n    array([[ 1,  4],\n           [ 5,  6],\n           [ 7,  9],\n           [10, 13]])\n\n    # If the second index is a slice or bool mask, the output is a ragged array.\n    # Even if each row is of the same length.\n    >>> ragged[:, :2]\n    RaggedArray.from_nested([\n        [1, 2],\n        [5, 6],\n        [7, 8],\n        [10, 11],\n    ])\n\nIf the second index is not a slice then the the output of getitem is a copy and\ndoes not share memory with the parent ragged array.\n\n\n3D (or higher) indices\n**********************\n\n`Higher Dimensional Arrays`_ can be sliced using 3 indices (or more).\n\nUsing another uninspiring enumeration example - this time a 3D array:\n\n.. code-block:: python\n\n    ragged = ragged_array([\n        [[ 0,  1,  2], [ 3,  4,  5]],\n        [[ 6,  7,  8], [ 9, 10, 11]],\n        [[12, 13, 14], [15, 16, 17], [18, 19, 20]],\n        [[21, 22, 23]],\n    ])\n\n3D arrays follow the same indexing rules as 2D arrays except that each **cell**\nis actually another array.\n\n.. code-block:: python\n\n    >>> ragged[0, 1]\n    array([3, 4, 5])\n\nAnd a triplet of indices are used to access individual elements.\n\n.. code-block:: python\n\n    >>> ragged[2, 0, 1]\n    13\n\n\nExport to standard types\n........................\n\nNo matter how many features I cram in to make ragged arrays more interchangeable\nwith normal ones,\nyou'll probably want to get back into regular array territory at the first\nopportunity.\n**rockhopper** comes with a few ways to do so.\n\nFirst, let us create a ragged array to export:\n\n.. code-block:: python\n\n    from rockhopper import ragged_array\n    ragged = ragged_array([\n        [1, 2, 3],\n        [4, 5, 6],\n        [7, 8],\n        [9, 10],\n        [11, 12, 13],\n    ])\n\n\nTo list of lists\n****************\n\nThe ``tolist()`` method converts back to nested lists (like those used to build\nthe array in the first place).\n\n.. code-block:: python\n\n    >>> ragged.tolist()\n    [[1, 2, 3], [4, 5, 6], [7, 8], [9, 10], [11, 12, 13]]\n\n\nTo list of homogenous arrays\n****************************\n\nWhen a ragged array is either not very ragged (row lengths are mostly the same)\nor not ragged at all (rows are all the same length),\nit's often helpful to split it on rows of differing lengths,\ngiving a sequence of standard rectangular arrays which can be ``for loop``\\ -ed\nover.\nDo this with the ``to_rectangular_arrays()`` method.\n\n.. code-block:: python\n\n    >>> ragged.to_rectangular_arrays()\n    [array([[1, 2, 3],\n            [4, 5, 6]]),\n     array([[7, 8],\n            [9, 10]]),\n     array([[11, 12, 13]])]\n\nIn the somewhat unlikely event that you don't care about the order the rows\nappear in,\nset the **reorder** option to allow it to presort the rows into ascending\nlengths so as to minimize fragmentation.\n\n.. code-block:: python\n\n    >>> sort_args, arrays = ragged.to_rectangular_arrays(reorder=True)\n    # The numpy.argsort() arguments are returned in case you want them.\n    >>> sort_args\n    array([2, 3, 0, 1, 4])\n    # By sorting, only 2 arrays are needed rather than 3.\n    >>> arrays\n    [array([[ 7,  8],\n            [ 9, 10]]),\n     array([[ 1,  2,  3],\n            [ 4,  5,  6],\n            [11, 12, 13]])]\n\n\nSerialisation and Deserialisation\n.................................\n\nRagged arrays may be converted to bytes and back again\nwhich can be read from or written to files.\n\n\nBinary\n******\n\nCurrently **rockhopper** knows of exactly one binary format:\nThe highly typical, but hopelessly un-NumPy-friendly::\n\n    row-length | row-content | row-length | row-content\n\nbinary form often found in 3D graphics\nwhere ``row-length`` may be any unsigned integer type of either byteorder,\n``row-content`` may be of any data type or byteorder,\nand there are no delimiters or metadata anywhere.\n\nFor this format ``RaggedArray()`` provides a ``loads()`` method for reading\nand a ``dumps()`` method for writing.\n\nSome examples:\n\n.. code-block:: python\n\n    # Write using:\n    #  - Row contents: The current data type (ragged.dtype) and endian.\n    #  - Row lengths: ``numpy.intc`` native endian\n    # Note that the output is a memoryview() which is generally interchangeable\n    # with bytes(). This may still be written to a file with the usual\n    # ``fh.write()``.\n    dumped = ragged.dumps()\n\n    # Read back using:\n    #  - Row contents: The same dtype used to write it\n    #  - Row lengths: ``numpy.intc`` native endian\n    ragged, bytes_consumed = RaggedArray.loads(dumped, ragged.dtype)\n\n    # Write then read using:\n    #  - Row contents: Big endian 8-byte floats\n    #  - Row lengths: Little endian 2-byte unsigned integers\n    dumped = ragged.astype(\">f8\").dumps(ldtype=\"<u2\")\n    ragged, bytes_consumed = RaggedArray.loads(dumped, \">f8\", ldtype=\"<u2\")\n\nBy default, ``loads()`` will keep adding rows until it hits the end of the byte\narray that it's parsing.\nThe ``bytes_consumed`` (a count of how many bytes from ``dumped`` where used)\nwill therefore always satisfy ``bytes_consumed == len(dumped)``.\n\nSome file formats contain a serialised ragged array embedded inside a larger\nfile but don't specify how many bytes belong to\nthe ragged array and how many belong to whatever comes afterwards.\nInstead they specify how many rows there should be.\nTo read such data use the **rows** keyword argument.\n\n.. code-block:: python\n\n    # Read a 20 row ragged array of floats from a long ``bytes()`` object called\n    # **blob**. Will raise an error if it runs out of data.\n    ragged, bytes_consumed = ragged.loads(blob, \"f8\", rows=20)\n\n    # ``bytes_consumed`` indicates where the ragged array stopped.\n    rest_of_blob = blob[bytes_consumed:]\n\n\nPickle\n******\n\nIf you don't need other programs to be able to read the output then bog-standard\npickle works too.\n\n.. code-block:: python\n\n    >>> import pickle\n    >>> arr = ragged_array([\n    ...    [\"cake\", \"biscuits\"],\n    ...    [\"socks\"],\n    ...    [\"orange\", \"lemon\", \"pineapple\"],\n    ... ])\n    >>> pickle.loads(pickle.dumps(arr))\n    RaggedArray.from_nested([\n        [\"cake\", \"biscuits\"],\n        [\"socks\"],\n        [\"orange\", \"lemon\", \"pineapple\"],\n    ])\n\n\nGrouping\n........\n\nArbitrary data may be grouped by some group enumeration into a ragged array so\nthat each data element appears on the row of its group number.\n\nFor example, to group the people in the following array...\n\n.. code-block:: python\n\n    people = np.array([\n        (\"Bob\", 1),\n        (\"Bill\", 2),\n        (\"Ben\", 0),\n        (\"Biff\", 1),\n        (\"Barnebas\", 0),\n        (\"Bubulous\", 1),\n        (\"Bofflodor\", 2),\n    ], dtype=[(\"name\", str, 20), (\"group number\", int)])\n\n... by the **group number** field use:\n\n.. code-block:: python\n\n    >>> from rockhopper import RaggedArray\n    >>> RaggedArray.group_by(people, people[\"group number\"])\n    RaggedArray.from_nested([\n        [('Ben', 0), ('Barnebas', 0)],\n        [('Bob', 1), ('Biff', 1), ('Bubulous', 1)],\n        [('Bill', 2), ('Bofflodor', 2)],\n    ])\n\nAs you can hopefully see,\n\n- all the names given a **group number** 0 appear in row 0,\n- all the names given a **group number** 1 appear in row 1,\n- and all the names given a **group number** 1 appear in row 2.\n\nAt this point you probably no longer care about the **group number** field,\nin which case, group only the **name** field:\n\n.. code-block:: python\n\n    >>> RaggedArray.group_by(people[\"name\"], people[\"group number\"])\n    RaggedArray.from_nested([\n        ['Ben', 'Barnebas'],\n        ['Bob', 'Biff', 'Bibulous'],\n        ['Bill', 'Bofflodor'],\n    ])\n\n\nEnumerating classes\n*******************\n\nThe above assumes that the parameter you wish to group by is just an\nenumeration.\nIf this is not the case, and you're not already sick of software written by me,\nthen you may use a `hirola.HashTable()\n<https://github.com/bwoodsend/Hirola#hirola>`_ to efficiently enumerate the\nparameter to group by.\n\nFor example, to group this list of animals by their animal class:\n\n.. code-block:: python\n\n    animals = np.array([\n        (\"cow\", \"mammal\"),\n        (\"moose\", \"mammal\"),\n        (\"centipede\", \"insect\"),\n        (\"robin\", \"bird\"),\n        (\"spider\", \"insect\"),\n        (\"whale\", \"mammal\"),\n        (\"woodpecker\", \"bird\"),\n    ], dtype=[(\"name\", str, 15), (\"class\", str, 15)])\n\nUse something like:\n\n.. code-block:: python\n\n    >>> from hirola import HashTable\n    >>> animal_classes = HashTable(len(animals), animals.dtype[\"class\"])\n    >>> enum = animal_classes.add(animals[\"class\"])\n\n    >>> RaggedArray.group_by(animals[\"name\"], enum)\n    RaggedArray.from_nested([\n        ['cow', 'moose', 'whale'],\n        ['centipede', 'spider'],\n        ['robin', 'woodpecker'],\n    ])\n    >>> animal_classes.keys\n    array(['mammal', 'insect', 'bird'], dtype='<U15')\n",
    "bugtrack_url": null,
    "license": "MIT license",
    "summary": "Ragged (rows with different lengths) 2D NumPy arrays.",
    "version": "0.2.0",
    "split_keywords": [
        "rockhopper"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "9409d4582b13c54dc7562580384a1cbb",
                "sha256": "deabea4281cfe9a9808e55271edcbffc6f7582dfa001826c96d74f327d9cba5f"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-macosx_10_6_x86_64.whl",
            "has_sig": false,
            "md5_digest": "9409d4582b13c54dc7562580384a1cbb",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 24004,
            "upload_time": "2022-06-30T22:16:58",
            "upload_time_iso_8601": "2022-06-30T22:16:58.769360Z",
            "url": "https://files.pythonhosted.org/packages/e1/2a/00d34f2f753494db9ad064d20cbb0313e4d7e971163edea13222ed563f5e/rockhopper-0.2.0-py3-none-macosx_10_6_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "cc11c5e63473f06613c3e0476bed26b9",
                "sha256": "60161c7c2d0b7a08adeac0bfbd709fac27afa18494b4ecfaa7f24552a67e5d26"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-macosx_11_0_arm64.whl",
            "has_sig": false,
            "md5_digest": "cc11c5e63473f06613c3e0476bed26b9",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 24060,
            "upload_time": "2022-06-30T22:17:01",
            "upload_time_iso_8601": "2022-06-30T22:17:01.203336Z",
            "url": "https://files.pythonhosted.org/packages/6f/66/ff095e3d8c3e695003db68d0fae4e4efb9e1f6cde7ececcf6c4ed36680b6/rockhopper-0.2.0-py3-none-macosx_11_0_arm64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "b59fca86db8d1b5fb456518681fd149d",
                "sha256": "4589831e450134e00dc0f2fa0469eb712bc85f27d1c16cec2383ab590dccddaa"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
            "has_sig": false,
            "md5_digest": "b59fca86db8d1b5fb456518681fd149d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 26744,
            "upload_time": "2022-06-30T22:17:08",
            "upload_time_iso_8601": "2022-06-30T22:17:08.502761Z",
            "url": "https://files.pythonhosted.org/packages/c2/66/add9bdfafa7b1f3bbb75867be8c23363f8a6a5c96863b36bc78500a52377/rockhopper-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "3adab878e6226ba3fe9cb29447179e97",
                "sha256": "d45627773fef8c139c1a1c5307ed96bb0260895e6c9b2cbfa85f4e30736b3bae"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "has_sig": false,
            "md5_digest": "3adab878e6226ba3fe9cb29447179e97",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 25972,
            "upload_time": "2022-06-30T22:17:10",
            "upload_time_iso_8601": "2022-06-30T22:17:10.750281Z",
            "url": "https://files.pythonhosted.org/packages/0f/d5/5eee2d4f1158e115ce3f4723ddfe433bc5f853a879f8e83761218d348894/rockhopper-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "31be9c2fd17319796c5c7cccd4b24c1a",
                "sha256": "aead0287415f8339b8d931f04f47c3538511736501da5926182d7c7d8a656187"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl",
            "has_sig": false,
            "md5_digest": "31be9c2fd17319796c5c7cccd4b24c1a",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 26646,
            "upload_time": "2022-06-30T22:17:03",
            "upload_time_iso_8601": "2022-06-30T22:17:03.399206Z",
            "url": "https://files.pythonhosted.org/packages/5d/d4/ade0d92042cf6faa51245b9b737a1ecc70a15bf026d34eb76a7d5b948a73/rockhopper-0.2.0-py3-none-manylinux_2_5_i686.manylinux1_i686.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "bd2001c3a4b140e221c0015dac3350c9",
                "sha256": "59584244b8edb986c52200229a0db7fc0aee6e6fee84d5dfb9939013dc500628"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl",
            "has_sig": false,
            "md5_digest": "bd2001c3a4b140e221c0015dac3350c9",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 26619,
            "upload_time": "2022-06-30T22:17:05",
            "upload_time_iso_8601": "2022-06-30T22:17:05.654590Z",
            "url": "https://files.pythonhosted.org/packages/9b/1a/e18716b0adc5d6cc9340230a47a617f965fb117afc847420d25615996de5/rockhopper-0.2.0-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "a24b255a65621d593e2b5f405479b0e5",
                "sha256": "28eb3609708e446ba5c31754f3a09fd2b1b3ddc8ee0a4a2559f158a7e1bf9a7c"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-musllinux_1_1_aarch64.whl",
            "has_sig": false,
            "md5_digest": "a24b255a65621d593e2b5f405479b0e5",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 26706,
            "upload_time": "2022-06-30T22:17:16",
            "upload_time_iso_8601": "2022-06-30T22:17:16.359207Z",
            "url": "https://files.pythonhosted.org/packages/c3/6e/64c5a8e086d61d537d089d6d52aace0b49822fe3d5ff625690c784d32e6b/rockhopper-0.2.0-py3-none-musllinux_1_1_aarch64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "a40ccc7c75e0d03354baff1bf6f4ec26",
                "sha256": "a9ab355ead21289c605a620aef1bd6ac55fafdee3278616d1cb6654bc820ddc3"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-musllinux_1_1_i686.whl",
            "has_sig": false,
            "md5_digest": "a40ccc7c75e0d03354baff1bf6f4ec26",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 27160,
            "upload_time": "2022-06-30T22:17:18",
            "upload_time_iso_8601": "2022-06-30T22:17:18.659493Z",
            "url": "https://files.pythonhosted.org/packages/50/91/2f58ced1432cd193a64c7ad171446528036f1da703d5c8ba68f2fde088ee/rockhopper-0.2.0-py3-none-musllinux_1_1_i686.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "18c60311c399ccd12840f2d75b457f3c",
                "sha256": "51339089660a2f875aef1f7025b40b3e237ff49236992c657d9cc30a1e6b3610"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-musllinux_1_1_x86_64.whl",
            "has_sig": false,
            "md5_digest": "18c60311c399ccd12840f2d75b457f3c",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 26697,
            "upload_time": "2022-06-30T22:17:20",
            "upload_time_iso_8601": "2022-06-30T22:17:20.667294Z",
            "url": "https://files.pythonhosted.org/packages/b8/0b/c8c99ed08382d5516180cf29d761de874b1f1ffe916e91c51caff4c7f2e6/rockhopper-0.2.0-py3-none-musllinux_1_1_x86_64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "a6e16b460bcd4f2d0a1f55b949da946d",
                "sha256": "1b7efeb773d5ce5de548365e8e69ad3084272f52a49a308f7a10237d9c4759d8"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-win32.whl",
            "has_sig": false,
            "md5_digest": "a6e16b460bcd4f2d0a1f55b949da946d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 34189,
            "upload_time": "2022-06-30T22:17:22",
            "upload_time_iso_8601": "2022-06-30T22:17:22.930400Z",
            "url": "https://files.pythonhosted.org/packages/b9/e0/15c14ba003ce266ba6dfad61c839eb1ab24f2cb1362f887eab3627b103a7/rockhopper-0.2.0-py3-none-win32.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "1a4244fe2d90fa72e10a0a69d4db26da",
                "sha256": "9469fa5fb8361cda439a84131fa950b827ee9f9bd69352d4db6cd15b4210d224"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0-py3-none-win_amd64.whl",
            "has_sig": false,
            "md5_digest": "1a4244fe2d90fa72e10a0a69d4db26da",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.6",
            "size": 34720,
            "upload_time": "2022-06-30T22:17:24",
            "upload_time_iso_8601": "2022-06-30T22:17:24.882364Z",
            "url": "https://files.pythonhosted.org/packages/26/f7/92cc964e5424b603f65ca4005630d63eff7ba0f43f8538a2d65770ce82dc/rockhopper-0.2.0-py3-none-win_amd64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "01a6425603782c134bc5e0b4389ec98c",
                "sha256": "470a396fdf8b85b77aed045fc90111b97595b5d88f30963dd32264bf8d80e905"
            },
            "downloads": -1,
            "filename": "rockhopper-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "01a6425603782c134bc5e0b4389ec98c",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.6",
            "size": 30196,
            "upload_time": "2022-06-30T22:17:26",
            "upload_time_iso_8601": "2022-06-30T22:17:26.655909Z",
            "url": "https://files.pythonhosted.org/packages/8d/a0/5dc6a9d367dd08b22dd3783afc54aa222dfde87b45a516504f62f19c2566/rockhopper-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-06-30 22:17:26",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "bwoodsend",
    "github_project": "rockhopper",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "lcname": "rockhopper"
}
        
Elapsed time: 0.37382s