An attempt at comingling async-code and nonasync-code in an ergonomic way.
*Latest release 20250103*:
* @afunc now accepts an async function and returns it unchanged.
* @agen now accepts an async generator and returns it unchanged.
* async_iter: now accepts an async iterable, return aiter(it) of it directly.
* New AnyIterable = Union[Iterable, AsyncIterable] type alias, to allow both sync and async iterators.
* async_iter: new optional fast=False parameter, if true then iterate the iterator directly instead of via asyncio.to_thread.
* async_iter: make missing `fast=` be True for list/tuple/set and False otherwise.
* @afunc: new optional fast=False parameter - if true then do not divert through asyncio.to_thread.
* New AsyncPipeLine, an asynchronous iterable with a `put` method to provide input for processing.
* New StageMode class with a STREAM enum for streaming stages, implement in AsyncPipeLine.run_stage.
* New aqget(Queue), an async interface to queue.Queue.get.
* New aqiter(Queue[,sentinel]), an async generator yielding from a queue.Queue.
One of the difficulties in adapting non-async code for use in
an async world is that anything asynchronous needs to be turtles
all the way down: a single blocking synchronous call anywhere
in the call stack blocks the async event loop.
This module presently provides:
- `@afunc`: a decorator to make a synchronous function asynchronous
- `@agen`: a decorator to make a synchronous generator asynchronous
- `amap(func,iterable)`: asynchronous mapping of `func` over an iterable
- `aqget(q)`: asynchronous function to get an item from a `queue.Queue` or similar
- `aqiter(q)`: asynchronous generator to yield items from a `queue.Queue` or similar
- `async_iter(iterable)`: return an asynchronous iterator of an iterable
- `IterableAsyncQueue`: an iterable flavour of `asyncio.Queue` with no `get` methods
- `AsyncPipeLine`: a pipeline of functions connected together with `IterableAsyncQueue`s
## <a name="afunc"></a>`afunc(*da, **dkw)`
A decorator for a synchronous function which turns it into
an asynchronous function.
If `func` is already an asynchronous function it is returned unchanged.
If `fast` is true (default `False`) then `func` is presumed to consume
negligible time and it is simply wrapped in an asynchronous function.
Otherwise it is wrapped in `asyncio.to_thread`.
Example:
@afunc
def func(count):
time.sleep(count)
return count
slept = await func(5)
@afunc(fast=True)
def asqrt(n):
return math.sqrt(n)
## <a name="agen"></a>`agen(*da, **dkw)`
A decorator for a synchronous generator which turns it into
an asynchronous generator.
If `genfunc` already an asynchronous generator it is returned unchanged.
Exceptions in the synchronous generator are reraised in the asynchronous
generator.
Example:
@agen
def gen(count):
for i in range(count):
yield i
time.sleep(1.0)
async for item in gen(5):
print(item)
## <a name="amap"></a>`amap(func: Callable[[Any], Any], it: Union[Iterable, AsyncIterable], concurrent=False, unordered=False, indexed=False)`
An asynchronous generator yielding the results of `func(item)`
for each `item` in the iterable `it`.
`it` may be a synchronous or asynchronous iterable.
`func` may be a synchronous or asynchronous callable.
If `concurrent` is `False` (the default), run each `func(item)`
call in series.
If `concurrent` is true run the function calls as `asyncio`
tasks concurrently.
If `unordered` is true (default `False`) yield results as
they arrive, otherwise yield results in the order of the items
in `it`, but as they arrive - tasks still evaluate concurrently
if `concurrent` is true.
If `indexed` is true (default `False`) yield 2-tuples of
`(i,result)` instead of just `result`, where `i` is the index
if each item from `it` counting from `0`.
Example of an async function to fetch URLs in parallel.
async def get_urls(urls : List[str]):
""" Fetch `urls` in parallel.
Yield `(url,response)` 2-tuples.
"""
async for i, response in amap(
requests.get, urls,
concurrent=True, unordered=True, indexed=True,
):
yield urls[i], response
## <a name="aqget"></a>`aqget(q: queue.Queue)`
An asynchronous function to get an item from a `queue.Queue`like object `q`.
It must support the `.get()` and `.get_nowait()` methods.
## <a name="aqiter"></a>`aqiter(q: queue.Queue, sentinel=<object object at 0x10f37a300>)`
An asynchronous generator to yield items from a `queue.Queue`like object `q`.
It must support the `.get()` and `.get_nowait()` methods.
An optional `sentinel` object may be supplied, which ends iteration
if encountered. If a sentinel is specified then this must be the only
consumer of the queue because the sentinel is consumed.
## <a name="async_iter"></a>`async_iter(it: Union[Iterable, AsyncIterable], fast=None)`
Return an asynchronous iterator yielding items from the iterable `it`.
An asynchronous iterable returns `aiter(it)` directly.
If `fast` is true then `it` is iterated directly instead of
via a distinct async generator. If not specified, `fast` is
set to `True` if `it` is a `list` or `tuple` or `set`. A true
value for this parameter indicates that fetching the next
item from `it` is always effectively instant and never blocks.
## <a name="AsyncPipeLine"></a>Class `AsyncPipeLine`
An `AsyncPipeLine` is an asynchronous iterable with a `put` method
to provide input for processing.
A new pipeline is usually constructed via the factory method
`AsyncPipeLine.from_stages(stage_func,...)`.
It has the same methods as an `IterableAsyncQueue`:
- `async put(item)` to queue an item for processing
- `async close()` to close the input, indicating end of the input items
- iteration to consume the processed results
It also has the following methods:
- `async submit(AnyIterable)` to submit multiple items for processing
- `async __call__(AnyIterable)` to submit the iterable for
processing and consume the results by iteration
Example:
def double(item):
yield item
yield item
pipeline = AsyncPipeLine.from_stages(
double,
double,
)
async for result in pipeline([1,2,3,4]):
print(result)
*`AsyncPipeLine.__call__(self, it: Union[Iterable, AsyncIterable], fast=None)`*:
Call the pipeline with an iterable.
*`AsyncPipeLine.close(self)`*:
Close the input queue.
*`AsyncPipeLine.from_stages(*stage_specs, maxsize=0) -> Tuple[cs.naysync.IterableAsyncQueue, cs.naysync.IterableAsyncQueue]`*:
Prepare an `AsyncPipeLine` from stage specifications.
Return `(inq,tasks,outq)` 3-tuple being an input `IterableAsyncQueue`
to receive items to process, a list of `asyncio.Task`s per
stage specification, and an output `IterableAsyncQueue` to
produce results. If there are no stage_specs the 2 queues
are the same queue.
Each stage specification is either:
- an stage function suitable for `run_stage`
- a 2-tuple of `(stage_func,batchsize)`
In the latter case:
- `stage_func` is an stage function suitable for `run_stage`
- `batchsize` is an `int`, where `0` means to gather all the
items from `inq` and supply them as a single batch to
`stage_func` and where a value `>0` collects items up to a limit
of `batchsize` and supplies each batch to `stage_func`
If the `batchsize` is `0` the `stage_func` is called exactly
once with all the input items, even if there are no input
items.
*`AsyncPipeLine.put(self, item)`*:
Put `item` onto the input queue.
*`AsyncPipeLine.run_stage(inq: cs.naysync.IterableAsyncQueue, stage_func, outq: cs.naysync.IterableAsyncQueue, batchsize: Union[int, NoneType, cs.naysync.StageMode] = None)`*:
Run a pipeline stage, copying items from `inq` to the `stage_func`
and putting results onto `outq`. After processing, `outq` is
closed.
`stage_func` is a callable which may be:
- a sync or async generator which yields results to place onto `outq`
- a sync or async function which returns a single result
If `batchsize` is `None`, the default, each input item is
passed to `stage_func(item)`, which yields the results from the
single item.
If `batchsize` is an `int`, items from `inq` are collected
into batches up to a limit of `batchsize` (no limit if
`batchsize` is `0`) and passed to `stage_func(batch)`, which
yields the results from the batch of items.
If the `batchsize` is `0` the `stage_func` is called exactly
once with all the input items, even if there are no input
items.
*`AsyncPipeLine.submit(self, it: Union[Iterable, AsyncIterable], fast=None)`*:
Submit the items from `it` to the pipeline.
## <a name="IterableAsyncQueue"></a>Class `IterableAsyncQueue(asyncio.queues.Queue)`
An iterable subclass of `asyncio.Queue`.
This modifies `asyncio.Queue` by:
- adding a `.close()` async method
- making the queue iterable, with each iteration consuming an item via `.get()`
*`IterableAsyncQueue.__anext__(self)`*:
Fetch the next item from the queue.
*`IterableAsyncQueue.close(self)`*:
Close the queue.
It is not an error to close the queue more than once.
*`IterableAsyncQueue.get(self)`*:
We do not allow `.get()`.
*`IterableAsyncQueue.get_nowat(self)`*:
We do not allow `.get_nowait()`.
*`IterableAsyncQueue.put(self, item)`*:
Put `item` onto the queue.
*`IterableAsyncQueue.put_nowait(self, item)`*:
Put an item onto the queue without blocking.
## <a name="StageMode"></a>Class `StageMode(enum.StrEnum)`
Special modes for `AsyncPipeLine` pipeline stages.
*`StageMode.__format__`*
*`StageMode.__str__`*
# Release Log
*Release 20250103*:
* @afunc now accepts an async function and returns it unchanged.
* @agen now accepts an async generator and returns it unchanged.
* async_iter: now accepts an async iterable, return aiter(it) of it directly.
* New AnyIterable = Union[Iterable, AsyncIterable] type alias, to allow both sync and async iterators.
* async_iter: new optional fast=False parameter, if true then iterate the iterator directly instead of via asyncio.to_thread.
* async_iter: make missing `fast=` be True for list/tuple/set and False otherwise.
* @afunc: new optional fast=False parameter - if true then do not divert through asyncio.to_thread.
* New AsyncPipeLine, an asynchronous iterable with a `put` method to provide input for processing.
* New StageMode class with a STREAM enum for streaming stages, implement in AsyncPipeLine.run_stage.
* New aqget(Queue), an async interface to queue.Queue.get.
* New aqiter(Queue[,sentinel]), an async generator yielding from a queue.Queue.
*Release 20241221.1*:
Doc fix for amap().
*Release 20241221*:
* Simpler implementation of @afunc.
* Simplify implementation of @agen by using async_iter.
* Docstring improvements.
*Release 20241220*:
* New async_iter(Iterable) returning an asynchronous iterator of a synchronous iterable.
* New amap(func,iterable) asynchronously mapping a function over an iterable.
*Release 20241215*:
* @afunc: now uses asyncio.to_thread() instead of wrapping @agen, drop the decorator parameters since no queue or polling are now used.
* @agen: nonpolling implementation - now uses asyncio.to_thread() for the next(genfunc) step, drop the decorator parameters since no queue or polling are now used.
*Release 20241214.1*:
Doc update.
*Release 20241214*:
Initial release with @agen and @afunc decorators.
Raw data
{
"_id": null,
"home_page": null,
"name": "cs-naysync",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "python3",
"author": null,
"author_email": "Cameron Simpson <cs@cskk.id.au>",
"download_url": "https://files.pythonhosted.org/packages/ff/eb/7d443029de92dc8c3c47d93c115461253f62f53a8eb0b814303c484c6e88/cs_naysync-20250103.tar.gz",
"platform": null,
"description": "An attempt at comingling async-code and nonasync-code in an ergonomic way.\n\n*Latest release 20250103*:\n* @afunc now accepts an async function and returns it unchanged.\n* @agen now accepts an async generator and returns it unchanged.\n* async_iter: now accepts an async iterable, return aiter(it) of it directly.\n* New AnyIterable = Union[Iterable, AsyncIterable] type alias, to allow both sync and async iterators.\n* async_iter: new optional fast=False parameter, if true then iterate the iterator directly instead of via asyncio.to_thread.\n* async_iter: make missing `fast=` be True for list/tuple/set and False otherwise.\n* @afunc: new optional fast=False parameter - if true then do not divert through asyncio.to_thread.\n* New AsyncPipeLine, an asynchronous iterable with a `put` method to provide input for processing.\n* New StageMode class with a STREAM enum for streaming stages, implement in AsyncPipeLine.run_stage.\n* New aqget(Queue), an async interface to queue.Queue.get.\n* New aqiter(Queue[,sentinel]), an async generator yielding from a queue.Queue.\n\nOne of the difficulties in adapting non-async code for use in\nan async world is that anything asynchronous needs to be turtles\nall the way down: a single blocking synchronous call anywhere\nin the call stack blocks the async event loop.\n\nThis module presently provides:\n- `@afunc`: a decorator to make a synchronous function asynchronous\n- `@agen`: a decorator to make a synchronous generator asynchronous\n- `amap(func,iterable)`: asynchronous mapping of `func` over an iterable\n- `aqget(q)`: asynchronous function to get an item from a `queue.Queue` or similar\n- `aqiter(q)`: asynchronous generator to yield items from a `queue.Queue` or similar\n- `async_iter(iterable)`: return an asynchronous iterator of an iterable\n- `IterableAsyncQueue`: an iterable flavour of `asyncio.Queue` with no `get` methods\n- `AsyncPipeLine`: a pipeline of functions connected together with `IterableAsyncQueue`s\n\n## <a name=\"afunc\"></a>`afunc(*da, **dkw)`\n\nA decorator for a synchronous function which turns it into\nan asynchronous function.\nIf `func` is already an asynchronous function it is returned unchanged.\nIf `fast` is true (default `False`) then `func` is presumed to consume\nnegligible time and it is simply wrapped in an asynchronous function.\nOtherwise it is wrapped in `asyncio.to_thread`.\n\nExample:\n\n @afunc\n def func(count):\n time.sleep(count)\n return count\n\n slept = await func(5)\n\n @afunc(fast=True)\n def asqrt(n):\n return math.sqrt(n)\n\n## <a name=\"agen\"></a>`agen(*da, **dkw)`\n\nA decorator for a synchronous generator which turns it into\nan asynchronous generator.\nIf `genfunc` already an asynchronous generator it is returned unchanged.\nExceptions in the synchronous generator are reraised in the asynchronous\ngenerator.\n\nExample:\n\n @agen\n def gen(count):\n for i in range(count):\n yield i\n time.sleep(1.0)\n\n async for item in gen(5):\n print(item)\n\n## <a name=\"amap\"></a>`amap(func: Callable[[Any], Any], it: Union[Iterable, AsyncIterable], concurrent=False, unordered=False, indexed=False)`\n\nAn asynchronous generator yielding the results of `func(item)`\nfor each `item` in the iterable `it`.\n\n`it` may be a synchronous or asynchronous iterable.\n\n`func` may be a synchronous or asynchronous callable.\n\nIf `concurrent` is `False` (the default), run each `func(item)`\ncall in series.\n\nIf `concurrent` is true run the function calls as `asyncio`\ntasks concurrently.\nIf `unordered` is true (default `False`) yield results as\nthey arrive, otherwise yield results in the order of the items\nin `it`, but as they arrive - tasks still evaluate concurrently\nif `concurrent` is true.\n\nIf `indexed` is true (default `False`) yield 2-tuples of\n`(i,result)` instead of just `result`, where `i` is the index\nif each item from `it` counting from `0`.\n\nExample of an async function to fetch URLs in parallel.\n\n async def get_urls(urls : List[str]):\n \"\"\" Fetch `urls` in parallel.\n Yield `(url,response)` 2-tuples.\n \"\"\"\n async for i, response in amap(\n requests.get, urls,\n concurrent=True, unordered=True, indexed=True,\n ):\n yield urls[i], response\n\n## <a name=\"aqget\"></a>`aqget(q: queue.Queue)`\n\nAn asynchronous function to get an item from a `queue.Queue`like object `q`.\nIt must support the `.get()` and `.get_nowait()` methods.\n\n## <a name=\"aqiter\"></a>`aqiter(q: queue.Queue, sentinel=<object object at 0x10f37a300>)`\n\nAn asynchronous generator to yield items from a `queue.Queue`like object `q`.\nIt must support the `.get()` and `.get_nowait()` methods.\n\nAn optional `sentinel` object may be supplied, which ends iteration\nif encountered. If a sentinel is specified then this must be the only\nconsumer of the queue because the sentinel is consumed.\n\n## <a name=\"async_iter\"></a>`async_iter(it: Union[Iterable, AsyncIterable], fast=None)`\n\nReturn an asynchronous iterator yielding items from the iterable `it`.\nAn asynchronous iterable returns `aiter(it)` directly.\n\nIf `fast` is true then `it` is iterated directly instead of\nvia a distinct async generator. If not specified, `fast` is\nset to `True` if `it` is a `list` or `tuple` or `set`. A true\nvalue for this parameter indicates that fetching the next\nitem from `it` is always effectively instant and never blocks.\n\n## <a name=\"AsyncPipeLine\"></a>Class `AsyncPipeLine`\n\nAn `AsyncPipeLine` is an asynchronous iterable with a `put` method\nto provide input for processing.\n\nA new pipeline is usually constructed via the factory method\n`AsyncPipeLine.from_stages(stage_func,...)`.\n\nIt has the same methods as an `IterableAsyncQueue`:\n- `async put(item)` to queue an item for processing\n- `async close()` to close the input, indicating end of the input items\n- iteration to consume the processed results\n\nIt also has the following methods:\n- `async submit(AnyIterable)` to submit multiple items for processing\n- `async __call__(AnyIterable)` to submit the iterable for\n processing and consume the results by iteration\n\n\nExample:\n\n def double(item):\n yield item\n yield item\n pipeline = AsyncPipeLine.from_stages(\n double,\n double,\n )\n async for result in pipeline([1,2,3,4]):\n print(result)\n\n*`AsyncPipeLine.__call__(self, it: Union[Iterable, AsyncIterable], fast=None)`*:\nCall the pipeline with an iterable.\n\n*`AsyncPipeLine.close(self)`*:\nClose the input queue.\n\n*`AsyncPipeLine.from_stages(*stage_specs, maxsize=0) -> Tuple[cs.naysync.IterableAsyncQueue, cs.naysync.IterableAsyncQueue]`*:\nPrepare an `AsyncPipeLine` from stage specifications.\nReturn `(inq,tasks,outq)` 3-tuple being an input `IterableAsyncQueue`\nto receive items to process, a list of `asyncio.Task`s per\nstage specification, and an output `IterableAsyncQueue` to\nproduce results. If there are no stage_specs the 2 queues\nare the same queue.\n\nEach stage specification is either:\n- an stage function suitable for `run_stage`\n- a 2-tuple of `(stage_func,batchsize)`\nIn the latter case:\n- `stage_func` is an stage function suitable for `run_stage`\n- `batchsize` is an `int`, where `0` means to gather all the\n items from `inq` and supply them as a single batch to\n `stage_func` and where a value `>0` collects items up to a limit\n of `batchsize` and supplies each batch to `stage_func`\nIf the `batchsize` is `0` the `stage_func` is called exactly\nonce with all the input items, even if there are no input\nitems.\n\n*`AsyncPipeLine.put(self, item)`*:\nPut `item` onto the input queue.\n\n*`AsyncPipeLine.run_stage(inq: cs.naysync.IterableAsyncQueue, stage_func, outq: cs.naysync.IterableAsyncQueue, batchsize: Union[int, NoneType, cs.naysync.StageMode] = None)`*:\nRun a pipeline stage, copying items from `inq` to the `stage_func`\nand putting results onto `outq`. After processing, `outq` is\nclosed.\n\n`stage_func` is a callable which may be:\n- a sync or async generator which yields results to place onto `outq`\n- a sync or async function which returns a single result\n\nIf `batchsize` is `None`, the default, each input item is\npassed to `stage_func(item)`, which yields the results from the\nsingle item.\n\nIf `batchsize` is an `int`, items from `inq` are collected\ninto batches up to a limit of `batchsize` (no limit if\n`batchsize` is `0`) and passed to `stage_func(batch)`, which\nyields the results from the batch of items.\nIf the `batchsize` is `0` the `stage_func` is called exactly\nonce with all the input items, even if there are no input\nitems.\n\n*`AsyncPipeLine.submit(self, it: Union[Iterable, AsyncIterable], fast=None)`*:\nSubmit the items from `it` to the pipeline.\n\n## <a name=\"IterableAsyncQueue\"></a>Class `IterableAsyncQueue(asyncio.queues.Queue)`\n\nAn iterable subclass of `asyncio.Queue`.\n\nThis modifies `asyncio.Queue` by:\n- adding a `.close()` async method\n- making the queue iterable, with each iteration consuming an item via `.get()`\n\n*`IterableAsyncQueue.__anext__(self)`*:\nFetch the next item from the queue.\n\n*`IterableAsyncQueue.close(self)`*:\nClose the queue.\nIt is not an error to close the queue more than once.\n\n*`IterableAsyncQueue.get(self)`*:\nWe do not allow `.get()`.\n\n*`IterableAsyncQueue.get_nowat(self)`*:\nWe do not allow `.get_nowait()`.\n\n*`IterableAsyncQueue.put(self, item)`*:\nPut `item` onto the queue.\n\n*`IterableAsyncQueue.put_nowait(self, item)`*:\nPut an item onto the queue without blocking.\n\n## <a name=\"StageMode\"></a>Class `StageMode(enum.StrEnum)`\n\nSpecial modes for `AsyncPipeLine` pipeline stages.\n\n*`StageMode.__format__`*\n\n*`StageMode.__str__`*\n\n# Release Log\n\n\n\n*Release 20250103*:\n* @afunc now accepts an async function and returns it unchanged.\n* @agen now accepts an async generator and returns it unchanged.\n* async_iter: now accepts an async iterable, return aiter(it) of it directly.\n* New AnyIterable = Union[Iterable, AsyncIterable] type alias, to allow both sync and async iterators.\n* async_iter: new optional fast=False parameter, if true then iterate the iterator directly instead of via asyncio.to_thread.\n* async_iter: make missing `fast=` be True for list/tuple/set and False otherwise.\n* @afunc: new optional fast=False parameter - if true then do not divert through asyncio.to_thread.\n* New AsyncPipeLine, an asynchronous iterable with a `put` method to provide input for processing.\n* New StageMode class with a STREAM enum for streaming stages, implement in AsyncPipeLine.run_stage.\n* New aqget(Queue), an async interface to queue.Queue.get.\n* New aqiter(Queue[,sentinel]), an async generator yielding from a queue.Queue.\n\n*Release 20241221.1*:\nDoc fix for amap().\n\n*Release 20241221*:\n* Simpler implementation of @afunc.\n* Simplify implementation of @agen by using async_iter.\n* Docstring improvements.\n\n*Release 20241220*:\n* New async_iter(Iterable) returning an asynchronous iterator of a synchronous iterable.\n* New amap(func,iterable) asynchronously mapping a function over an iterable.\n\n*Release 20241215*:\n* @afunc: now uses asyncio.to_thread() instead of wrapping @agen, drop the decorator parameters since no queue or polling are now used.\n* @agen: nonpolling implementation - now uses asyncio.to_thread() for the next(genfunc) step, drop the decorator parameters since no queue or polling are now used.\n\n*Release 20241214.1*:\nDoc update.\n\n*Release 20241214*:\nInitial release with @agen and @afunc decorators.\n",
"bugtrack_url": null,
"license": "GNU General Public License v3 or later (GPLv3+)",
"summary": "An attempt at comingling async-code and nonasync-code in an ergonomic way.",
"version": "20250103",
"project_urls": {
"MonoRepo Commits": "https://bitbucket.org/cameron_simpson/css/commits/branch/main",
"Monorepo Git Mirror": "https://github.com/cameron-simpson/css",
"Monorepo Hg/Mercurial Mirror": "https://hg.sr.ht/~cameron-simpson/css",
"Source": "https://github.com/cameron-simpson/css/blob/main/lib/python/cs/naysync.py"
},
"split_keywords": [
"python3"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "9a99ee1a3c6318a40e415a4decfdccbcd09f91823cf3c519bff0e37963540765",
"md5": "2f2ce74ea9208de0d17638b0027fe79f",
"sha256": "cfc8d1c0994d4f606548bb1826baf5f98f8b802c912135528213ec8e8073a700"
},
"downloads": -1,
"filename": "cs_naysync-20250103-py3-none-any.whl",
"has_sig": false,
"md5_digest": "2f2ce74ea9208de0d17638b0027fe79f",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 11073,
"upload_time": "2025-01-03T04:32:57",
"upload_time_iso_8601": "2025-01-03T04:32:57.403561Z",
"url": "https://files.pythonhosted.org/packages/9a/99/ee1a3c6318a40e415a4decfdccbcd09f91823cf3c519bff0e37963540765/cs_naysync-20250103-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "ffeb7d443029de92dc8c3c47d93c115461253f62f53a8eb0b814303c484c6e88",
"md5": "ad87c4df065612ac56bba7f8ea918c28",
"sha256": "9c51be6bffb1603d09efd0223aa8833e1e3935dec978635755447842b09f8990"
},
"downloads": -1,
"filename": "cs_naysync-20250103.tar.gz",
"has_sig": false,
"md5_digest": "ad87c4df065612ac56bba7f8ea918c28",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 11587,
"upload_time": "2025-01-03T04:33:02",
"upload_time_iso_8601": "2025-01-03T04:33:02.134170Z",
"url": "https://files.pythonhosted.org/packages/ff/eb/7d443029de92dc8c3c47d93c115461253f62f53a8eb0b814303c484c6e88/cs_naysync-20250103.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-01-03 04:33:02",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "cameron-simpson",
"github_project": "css",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "cs-naysync"
}