[![Build Status](https://github.com/medialab/quenouille/workflows/Tests/badge.svg)](https://github.com/medialab/quenouille/actions)
# Quenouille
A library of multithreaded iterator workflows for python.
It is typically used to iterate over lazy streams without overflowing memory, all while respecting group parallelism constraints and throttling, e.g. when downloading massive amounts of urls from the web concurrently.
It is mainly used by the [minet](https://github.com/medialab/minet) python library and CLI tool to power its downloaders and crawlers.
## Installation
You can install `quenouille` with pip using the following command:
```
pip install quenouille
```
## Usage
- [imap, imap_unordered](#imap_unordered-imap)
- [ThreadPoolExecutor](#threadpoolexecutor)
- [#.imap, #.imap_unordered](#executor-imap)
- [#.shutdown](#shutdown)
- [NamedLocks](#namedlocks)
- [Miscellaneous notes](#miscellaneous-notes)
- [The None group](#the-none-group)
- [Parallelism > workers](#parallelism--workers)
- [Callable parallelism guarantees](#callable-parallelism-guarantees)
- [Parallelism vs. throttling](#parallelism-vs-throttling)
- [Adding entropy to throttle](#adding-entropy-to-throttle)
- [Caveats regarding exception raising](#caveats-regarding-exception-raising)
- [Caveats of using imap with queues](#caveats-of-using-imap-with-queues)
### imap, imap_unordered
Function lazily consuming an iterable and applying the desired function over the yielded items in a multithreaded fashion.
This function is also able to respect group constraints regarding parallelism and throttling: for instance, if you need to download urls in a multithreaded fashion and need to ensure that you won't hit the same domain more than twice at the same time, this function can be given proper settings to ensure the desired behavior.
The same can be said if you need to make sure to wait for a certain amount of time between two hits on the same domain by using this function's throttling.
Finally note that this function comes in two flavors: `imap_unordered`, which will output processed items as soon as they are done, and `imap`, which will output items in the same order as the input, at the price of slightly worse performance and an increased memory footprint that depends on the number of items that have been processed before the next item can be yielded.
```python
import csv
from quenouille import imap, imap_unordered
# Reading urls lazily from a CSV file using a generator:
def urls():
with open('urls.csv') as f:
reader = csv.DictReader(f)
for line in reader:
yield line['url']
# Defining some functions
def fetch(url):
# ... e.g. use urllib3 to download the url
# remember this function must be threadsafe
return html
def get_domain_name(url):
# ... e.g. use ural to extract domain name from url
return domain_name
# Performing 10 requests at a time:
for html in imap(urls(), fetch, 10):
print(html)
# Ouputting results as soon as possible (in arbitrary order)
for html in imap_unordered(urls(), fetch, 10):
print(html)
# Ensuring we don't hit the same domain more that twice at a time
for html in imap(urls(), fetch, 10, key=get_domain_name, parallelism=2):
print(html)
# Waiting 5 seconds between each request on a same domain
for html in imap(urls(), fetch, 10, key=get_domain_name, throttle=5):
print(html)
# Throttle time depending on domain
def throttle(group, item, result):
if group == 'lemonde.fr':
return 10
return 2
for html in imap(urls(), fetch, 10, key=get_domain_name, throttle=throttle):
print(html)
# Only load 10 urls into memory when attempting to find next suitable job
for html in imap(urls(), fetch, 10, key=get_domain_name, throttle=5, buffer_size=10):
print(html)
```
_Arguments_
- **iterable** _(iterable)_: Any python iterable.
- **func** _(callable)_: Function used to perform the desired tasks. The function takes an item yielded by the given iterable as single argument. Note that since this function will be dispatched in worker threads, so you should ensure it is thread-safe.
- **threads** _(int, optional)_: Maximum number of threads to use. Defaults to `min(32, os.cpu_count() + 1)`. Note that it can be `0`, in which case no threads will be used and everything will run synchronously (this can be useful for debugging or to avoid duplicating code sometimes).
- **key** _(callable, optional)_: Function returning to which "group" a given item is supposed to belong. This will be used to ensure maximum parallelism is respected.
- **parallelism** _(int or callable, optional)_ [`1`]: Number of threads allowed to work on a same group at once. Can also be a function taking a group and returning its parallelism.
- **buffer_size** _(int, optional)_ [`1024`]: Maximum number of items the function will buffer into memory while attempting to find an item that can be passed to a worker immediately, all while respecting throttling and group parallelism.
- **throttle** _(int or float or callable, optional)_: Optional throttle time, in seconds, to wait before processing the next item of a given group. Can also be a function taking last group, item and result and returning next throttle time for this group.
- **block** _(bool, optional)_ [`False`]: Whether to block when using the `get` method of the given queue.
- **panic** _(callable, optional)_: Function that will be called when the process has errored. Useful to unblock some functions and avoid deadlock in specific situations. Note that this function will not be called when synchronous (i.e. when not using additional threads).
- **initializer** _(callable, optional)_: Function to run at the start of each thread worker. Can be useful to setup [thread-local data](https://docs.python.org/3/library/threading.html#thread-local-data), for instance. Remember this function must be threadsafe and should not block because the thread pool will wait for each thread to be correctly booted before being able to proceed. If one of the function calls fails, the thread pool will raise a `quenouille.exceptions.BrokenThreadPool` error and terminate immediately.
- **initargs** _(iterable, optional)_: Arguments to pass to the `initializer` function.
- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when shutting down the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.
- **daemonic** _(bool, optional)_ [`False`]: whether to spawn daemonic worker. If your worker are daemonic, the interpreter will not wait for them to end when exiting. This can be useful, combined to `wait=False`, for instance, if you want your program to exit as soon as hitting ctrl+C (you might want to avoid this if your threads need to cleanup things on exit as they will be abruptly shut down).
_Using a queue rather than an iterable_
If you need to add new items to process as a result of performing tasks (when designing a web crawler for instance, where each downloaded page will yield new pages to explore further down), know that the `imap` and `imap_unordered` function also accepts queues as input:
```python
from queue import Queue
from quenouille import imap
job_queue = Queue()
job_queue.put(1)
# Enqueuing new jobs in the worker
def worker(i):
if i < 3:
job_queue.put(i + 1)
return i * 2
list(imap(job_queue, worker, 2))
>>> [2, 4, 6]
# Enqueuing new jobs while iterating over results
job_queue = Queue()
job_queue.put(1)
results = []
for i in imap(job_queue, worker, 2):
if i < 5:
job_queue.put((i / 2) + 1)
results.append(i)
results
>>> [2, 4, 6]
```
Note that `imap` will only run until the queue is fully drained. So if you decide to add more items afterwards, this is not the function's concern anymore.
### ThreadPoolExecutor
If you need to run `imap` or `imap_unordered` multiple times in succession while keeping the same thread pool up and running, you can also directly use quenouille's `ThreadPoolExecutor` like so:
```python
from quenouille import ThreadPoolExecutor
# Using it as a context manager:
with ThreadPoolExecutor(max_workers=4) as executor:
for i in executor.imap(range(10), worker):
print(i)
for j in executor.imap_unordered(range(10), worker):
print(j)
# Or if you prefer shutting down the executor explicitly:
executor = ThreadPoolExecutor()
executor.imap(range(10), worker)
executor.shutdown(wait=False)
```
Note that your throttling state is kept between multiple `imap` and `imap_unordered` calls so you don't end up perform some tasks too soon. But keep in mind this state is tied to the `key` function you provide to remain consistent, so if you change the used `key`, the throttling state will be reset.
_Arguments_
- **max_workers** _(int, optional)_: Maximum number of threads to use. Defaults to `min(32, os.cpu_count() + 1)`. Note that it can be `0`, in which case no threads will be used and everything will run synchronously (this can be useful for debugging or to avoid duplicating code sometimes).
- **initializer** _(callable, optional)_: Function to run at the start of each thread worker. Can be useful to setup [thread-local data](https://docs.python.org/3/library/threading.html#thread-local-data), for instance. Remember this function must be threadsafe and should not block because the thread pool will wait for each thread to be correctly booted before being able to proceed. If one of the function calls fails, the thread pool will raise a `quenouille.exceptions.BrokenThreadPool` error and terminate immediately.
- **initargs** _(iterable, optional)_: Arguments to pass to the `initializer` function.
- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when closing the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.
- **daemonic** _(bool, optional)_ [`False`]: whether to spawn daemonic worker. If your worker are daemonic, the interpreter will not wait for them to end when exiting. This can be useful, combined to `wait=False`, for instance, if you want your program to exit as soon as hitting ctrl+C (you might want to avoid this if your threads need to cleanup things on exit as they will be abruptly shut down).
<h4 id="executor-imap">#.imap, #.imap_unordered</h4>
Basically the same as described [here](#imap_unordered-imap) with the following arguments:
- **iterable** _(iterable)_: Any python iterable.
- **func** _(callable)_: Function used to perform the desired tasks. The function takes an item yielded by the given iterable as single argument. Note that since this function will be dispatched in worker threads, so you should ensure it is thread-safe.
- **key** _(callable, optional)_: Function returning to which "group" a given item is supposed to belong. This will be used to ensure maximum parallelism is respected.
- **parallelism** _(int or callable, optional)_ [`1`]: Number of threads allowed to work on a same group at once. Can also be a function taking a group and returning its parallelism.
- **buffer_size** _(int, optional)_ [`1024`]: Maximum number of items the function will buffer into memory while attempting to find an item that can be passed to a worker immediately, all while respecting throttling and group parallelism.
- **throttle** _(int or float or callable, optional)_: Optional throttle time, in seconds, to wait before processing the next item of a given group. Can also be a function taking last group, item and result and returning next throttle time for this group.
- **block** _(bool, optional)_ [`False`]: Whether to block when using the `get` method of the given queue.
- **panic** _(callable, optional)_: Function that will be called when the process has errored. Useful to unblock some functions and avoid deadlock in specific situations. Note that this function will not be called when synchronous (i.e. when not using additional threads).
#### #.shutdown
Method used to explicitly shutdown the executor.
_Arguments_
- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when shutting down the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.
### NamedLocks
A weakref dictionary of locks useful to make some tasks based on keys threadsafe, e.g. if you need to ensure that two threads will not be writing to the same file at once.
```python
from quenouille import NamedLocks
locks = NamedLocks()
def worker(filename):
with locks[filename]:
with open(filename, 'a+') as f:
f.write('hello\n')
```
### Miscellaneous notes
#### The None group
The `imap` functions consider the `None` group (this can happen if your `key` function returns `None`) as special and will always consider the attached items can be processed right away without parallelism constraints nor throttle.
Without `key`, all items are considered as belonging to the `None` group.
#### Parallelism > workers
If you set `parallelism` to `10` and only allocate `5` threads to perform your tasks, it should be obvious that the actual parallelism will never reach `10`.
`quenouille` won't warn you of this because it might be convenient not to give this too much thought and has not much consequence, but keep this in mind.
#### Callable parallelism guarantees
If you decide to pass a function to declare a custom parallelism for each group rather than a global fixed one, know that the function will be called each time `quenouille` has to make a decision regarding the next items to enqueue so that we don't use any memory to record the information.
This means that you should guarantee that the given function is idempotent and will always return the same parallelism for a given group if you don't want to risk a deadlock.
#### Parallelism vs. throttling
If a group is being trottled, it should be obvious that `quenouille` won't perform more than one single task for this group at once, so its `parallelism` is in fact `1`, in spite of other settings.
This does not mean that `parallelism` for some groups and `throttle` for others is impossible. This can be achieved through callables.
#### Adding entropy to throttle
You can understand the callable `throttle` kwarg as "what's the minimum time the next job from this group should wait before firing up". This means that if you need to add entropy to the throttle time, you can indeed make this function work with randomness like so:
```python
from random import random
# Waiting 5 + (between 0 and 2) seconds
def throttle(group, item, result):
return 5 + (2 * random())
```
#### Caveats regarding exception raising
_Deferred generator usage exception deadlocks_
If you consume a generator returned by `imap/imap_unordered` somewhere else than where you created it, you may end up in a deadlock if you raise an exception.
This is not important when using `daemonic=True` but you might stumble upon segfaults on exit because of python reasons beyond my control.
```python
# Safe
for item in imap(...):
raise RuntimeError
# Not safe
it = imap(...)
for item in it:
raise RuntimeError
```
If you really want to do that, because you don't want to use the `ThreadPoolExecutor` context manager, you can try using the experimental `excepthook` kwarg:
```python
# Probably safe
it = imap(..., excepthook=True)
for item in it:
raise RuntimeError
```
#### Caveats of using imap with queues
_Typical deadlocks_
Even if `imap` can process an input queue, you should avoid to find yourself in a situation where adding to the queue might block execution if you don't want to end in a deadlock. It can be easy to footgun yourself if your queue has a `maxsize`, for instance:
```python
from queue import Queue
from quenouille import imap
job_queue = Queue(maxsize=2)
job_queue.put(1)
for i in imap(job_queue, worker):
if i < 2:
job_queue.put(2)
job_queue.put(3)
# This will put you in a deadlock because this will block
# because of the queue `maxsize` set to 2
job_queue.put(4)
print(i)
```
_Design choices_
To enable you to add items to the queue in the loop body and so it can safely detect when your queue is drained without race condition, `quenouille` acknowledges that a task is finished only after what you execute in the loop body is done.
This means that sometimes it might be more performant to only add items to the queue from the worker functions rather than from the loop body.
_queue.task_done_
For now, `quenouille` does not call `queue.task_done` for you, so this remains your responsability, if you want to be able to call `queue.join` down the line.
Raw data
{
"_id": null,
"home_page": "http://github.com/medialab/quenouille",
"name": "quenouille",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.5",
"maintainer_email": "",
"keywords": "url",
"author": "Guillaume Plique",
"author_email": "kropotkinepiotr@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/4f/ba/27a48ce300988735ac5768ef54b307d0a90dd6ea5abd74c1941ac5ea6b54/quenouille-1.9.1.tar.gz",
"platform": null,
"description": "[![Build Status](https://github.com/medialab/quenouille/workflows/Tests/badge.svg)](https://github.com/medialab/quenouille/actions)\n\n# Quenouille\n\nA library of multithreaded iterator workflows for python.\n\nIt is typically used to iterate over lazy streams without overflowing memory, all while respecting group parallelism constraints and throttling, e.g. when downloading massive amounts of urls from the web concurrently.\n\nIt is mainly used by the [minet](https://github.com/medialab/minet) python library and CLI tool to power its downloaders and crawlers.\n\n## Installation\n\nYou can install `quenouille` with pip using the following command:\n\n```\npip install quenouille\n```\n\n## Usage\n\n- [imap, imap_unordered](#imap_unordered-imap)\n- [ThreadPoolExecutor](#threadpoolexecutor)\n - [#.imap, #.imap_unordered](#executor-imap)\n - [#.shutdown](#shutdown)\n- [NamedLocks](#namedlocks)\n- [Miscellaneous notes](#miscellaneous-notes)\n - [The None group](#the-none-group)\n - [Parallelism > workers](#parallelism--workers)\n - [Callable parallelism guarantees](#callable-parallelism-guarantees)\n - [Parallelism vs. throttling](#parallelism-vs-throttling)\n - [Adding entropy to throttle](#adding-entropy-to-throttle)\n - [Caveats regarding exception raising](#caveats-regarding-exception-raising)\n - [Caveats of using imap with queues](#caveats-of-using-imap-with-queues)\n\n### imap, imap_unordered\n\nFunction lazily consuming an iterable and applying the desired function over the yielded items in a multithreaded fashion.\n\nThis function is also able to respect group constraints regarding parallelism and throttling: for instance, if you need to download urls in a multithreaded fashion and need to ensure that you won't hit the same domain more than twice at the same time, this function can be given proper settings to ensure the desired behavior.\n\nThe same can be said if you need to make sure to wait for a certain amount of time between two hits on the same domain by using this function's throttling.\n\nFinally note that this function comes in two flavors: `imap_unordered`, which will output processed items as soon as they are done, and `imap`, which will output items in the same order as the input, at the price of slightly worse performance and an increased memory footprint that depends on the number of items that have been processed before the next item can be yielded.\n\n```python\nimport csv\nfrom quenouille import imap, imap_unordered\n\n# Reading urls lazily from a CSV file using a generator:\ndef urls():\n with open('urls.csv') as f:\n reader = csv.DictReader(f)\n\n for line in reader:\n yield line['url']\n\n# Defining some functions\ndef fetch(url):\n # ... e.g. use urllib3 to download the url\n # remember this function must be threadsafe\n return html\n\ndef get_domain_name(url):\n # ... e.g. use ural to extract domain name from url\n return domain_name\n\n# Performing 10 requests at a time:\nfor html in imap(urls(), fetch, 10):\n print(html)\n\n# Ouputting results as soon as possible (in arbitrary order)\nfor html in imap_unordered(urls(), fetch, 10):\n print(html)\n\n# Ensuring we don't hit the same domain more that twice at a time\nfor html in imap(urls(), fetch, 10, key=get_domain_name, parallelism=2):\n print(html)\n\n# Waiting 5 seconds between each request on a same domain\nfor html in imap(urls(), fetch, 10, key=get_domain_name, throttle=5):\n print(html)\n\n# Throttle time depending on domain\ndef throttle(group, item, result):\n if group == 'lemonde.fr':\n return 10\n\n return 2\n\nfor html in imap(urls(), fetch, 10, key=get_domain_name, throttle=throttle):\n print(html)\n\n# Only load 10 urls into memory when attempting to find next suitable job\nfor html in imap(urls(), fetch, 10, key=get_domain_name, throttle=5, buffer_size=10):\n print(html)\n```\n\n_Arguments_\n\n- **iterable** _(iterable)_: Any python iterable.\n- **func** _(callable)_: Function used to perform the desired tasks. The function takes an item yielded by the given iterable as single argument. Note that since this function will be dispatched in worker threads, so you should ensure it is thread-safe.\n- **threads** _(int, optional)_: Maximum number of threads to use. Defaults to `min(32, os.cpu_count() + 1)`. Note that it can be `0`, in which case no threads will be used and everything will run synchronously (this can be useful for debugging or to avoid duplicating code sometimes).\n- **key** _(callable, optional)_: Function returning to which \"group\" a given item is supposed to belong. This will be used to ensure maximum parallelism is respected.\n- **parallelism** _(int or callable, optional)_ [`1`]: Number of threads allowed to work on a same group at once. Can also be a function taking a group and returning its parallelism.\n- **buffer_size** _(int, optional)_ [`1024`]: Maximum number of items the function will buffer into memory while attempting to find an item that can be passed to a worker immediately, all while respecting throttling and group parallelism.\n- **throttle** _(int or float or callable, optional)_: Optional throttle time, in seconds, to wait before processing the next item of a given group. Can also be a function taking last group, item and result and returning next throttle time for this group.\n- **block** _(bool, optional)_ [`False`]: Whether to block when using the `get` method of the given queue.\n- **panic** _(callable, optional)_: Function that will be called when the process has errored. Useful to unblock some functions and avoid deadlock in specific situations. Note that this function will not be called when synchronous (i.e. when not using additional threads).\n- **initializer** _(callable, optional)_: Function to run at the start of each thread worker. Can be useful to setup [thread-local data](https://docs.python.org/3/library/threading.html#thread-local-data), for instance. Remember this function must be threadsafe and should not block because the thread pool will wait for each thread to be correctly booted before being able to proceed. If one of the function calls fails, the thread pool will raise a `quenouille.exceptions.BrokenThreadPool` error and terminate immediately.\n- **initargs** _(iterable, optional)_: Arguments to pass to the `initializer` function.\n- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when shutting down the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.\n- **daemonic** _(bool, optional)_ [`False`]: whether to spawn daemonic worker. If your worker are daemonic, the interpreter will not wait for them to end when exiting. This can be useful, combined to `wait=False`, for instance, if you want your program to exit as soon as hitting ctrl+C (you might want to avoid this if your threads need to cleanup things on exit as they will be abruptly shut down).\n\n_Using a queue rather than an iterable_\n\nIf you need to add new items to process as a result of performing tasks (when designing a web crawler for instance, where each downloaded page will yield new pages to explore further down), know that the `imap` and `imap_unordered` function also accepts queues as input:\n\n```python\nfrom queue import Queue\nfrom quenouille import imap\n\njob_queue = Queue()\njob_queue.put(1)\n\n# Enqueuing new jobs in the worker\ndef worker(i):\n if i < 3:\n job_queue.put(i + 1)\n\n return i * 2\n\nlist(imap(job_queue, worker, 2))\n>>> [2, 4, 6]\n\n# Enqueuing new jobs while iterating over results\njob_queue = Queue()\njob_queue.put(1)\n\nresults = []\n\nfor i in imap(job_queue, worker, 2):\n if i < 5:\n job_queue.put((i / 2) + 1)\n\n results.append(i)\n\nresults\n>>> [2, 4, 6]\n```\n\nNote that `imap` will only run until the queue is fully drained. So if you decide to add more items afterwards, this is not the function's concern anymore.\n\n### ThreadPoolExecutor\n\nIf you need to run `imap` or `imap_unordered` multiple times in succession while keeping the same thread pool up and running, you can also directly use quenouille's `ThreadPoolExecutor` like so:\n\n```python\nfrom quenouille import ThreadPoolExecutor\n\n# Using it as a context manager:\nwith ThreadPoolExecutor(max_workers=4) as executor:\n for i in executor.imap(range(10), worker):\n print(i)\n\n for j in executor.imap_unordered(range(10), worker):\n print(j)\n\n# Or if you prefer shutting down the executor explicitly:\nexecutor = ThreadPoolExecutor()\nexecutor.imap(range(10), worker)\nexecutor.shutdown(wait=False)\n```\n\nNote that your throttling state is kept between multiple `imap` and `imap_unordered` calls so you don't end up perform some tasks too soon. But keep in mind this state is tied to the `key` function you provide to remain consistent, so if you change the used `key`, the throttling state will be reset.\n\n_Arguments_\n\n- **max_workers** _(int, optional)_: Maximum number of threads to use. Defaults to `min(32, os.cpu_count() + 1)`. Note that it can be `0`, in which case no threads will be used and everything will run synchronously (this can be useful for debugging or to avoid duplicating code sometimes).\n- **initializer** _(callable, optional)_: Function to run at the start of each thread worker. Can be useful to setup [thread-local data](https://docs.python.org/3/library/threading.html#thread-local-data), for instance. Remember this function must be threadsafe and should not block because the thread pool will wait for each thread to be correctly booted before being able to proceed. If one of the function calls fails, the thread pool will raise a `quenouille.exceptions.BrokenThreadPool` error and terminate immediately.\n- **initargs** _(iterable, optional)_: Arguments to pass to the `initializer` function.\n- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when closing the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.\n- **daemonic** _(bool, optional)_ [`False`]: whether to spawn daemonic worker. If your worker are daemonic, the interpreter will not wait for them to end when exiting. This can be useful, combined to `wait=False`, for instance, if you want your program to exit as soon as hitting ctrl+C (you might want to avoid this if your threads need to cleanup things on exit as they will be abruptly shut down).\n\n<h4 id=\"executor-imap\">#.imap, #.imap_unordered</h4>\n\nBasically the same as described [here](#imap_unordered-imap) with the following arguments:\n\n- **iterable** _(iterable)_: Any python iterable.\n- **func** _(callable)_: Function used to perform the desired tasks. The function takes an item yielded by the given iterable as single argument. Note that since this function will be dispatched in worker threads, so you should ensure it is thread-safe.\n- **key** _(callable, optional)_: Function returning to which \"group\" a given item is supposed to belong. This will be used to ensure maximum parallelism is respected.\n- **parallelism** _(int or callable, optional)_ [`1`]: Number of threads allowed to work on a same group at once. Can also be a function taking a group and returning its parallelism.\n- **buffer_size** _(int, optional)_ [`1024`]: Maximum number of items the function will buffer into memory while attempting to find an item that can be passed to a worker immediately, all while respecting throttling and group parallelism.\n- **throttle** _(int or float or callable, optional)_: Optional throttle time, in seconds, to wait before processing the next item of a given group. Can also be a function taking last group, item and result and returning next throttle time for this group.\n- **block** _(bool, optional)_ [`False`]: Whether to block when using the `get` method of the given queue.\n- **panic** _(callable, optional)_: Function that will be called when the process has errored. Useful to unblock some functions and avoid deadlock in specific situations. Note that this function will not be called when synchronous (i.e. when not using additional threads).\n\n#### #.shutdown\n\nMethod used to explicitly shutdown the executor.\n\n_Arguments_\n\n- **wait** _(bool, optional)_ [`True`]: Whether to join worker threads, i.e. wait for them to end, when shutting down the executor. Set this to `False` if you need to go on quickly without waiting for your worker threads to end when cleaning up the executor's resources. Just note that if you spawn other thread-intensive tasks or other executors afterwards in rapid succession, you might start too many threads at once.\n\n### NamedLocks\n\nA weakref dictionary of locks useful to make some tasks based on keys threadsafe, e.g. if you need to ensure that two threads will not be writing to the same file at once.\n\n```python\nfrom quenouille import NamedLocks\n\nlocks = NamedLocks()\n\ndef worker(filename):\n with locks[filename]:\n with open(filename, 'a+') as f:\n f.write('hello\\n')\n```\n\n### Miscellaneous notes\n\n#### The None group\n\nThe `imap` functions consider the `None` group (this can happen if your `key` function returns `None`) as special and will always consider the attached items can be processed right away without parallelism constraints nor throttle.\n\nWithout `key`, all items are considered as belonging to the `None` group.\n\n#### Parallelism > workers\n\nIf you set `parallelism` to `10` and only allocate `5` threads to perform your tasks, it should be obvious that the actual parallelism will never reach `10`.\n\n`quenouille` won't warn you of this because it might be convenient not to give this too much thought and has not much consequence, but keep this in mind.\n\n#### Callable parallelism guarantees\n\nIf you decide to pass a function to declare a custom parallelism for each group rather than a global fixed one, know that the function will be called each time `quenouille` has to make a decision regarding the next items to enqueue so that we don't use any memory to record the information.\n\nThis means that you should guarantee that the given function is idempotent and will always return the same parallelism for a given group if you don't want to risk a deadlock.\n\n#### Parallelism vs. throttling\n\nIf a group is being trottled, it should be obvious that `quenouille` won't perform more than one single task for this group at once, so its `parallelism` is in fact `1`, in spite of other settings.\n\nThis does not mean that `parallelism` for some groups and `throttle` for others is impossible. This can be achieved through callables.\n\n#### Adding entropy to throttle\n\nYou can understand the callable `throttle` kwarg as \"what's the minimum time the next job from this group should wait before firing up\". This means that if you need to add entropy to the throttle time, you can indeed make this function work with randomness like so:\n\n```python\nfrom random import random\n\n# Waiting 5 + (between 0 and 2) seconds\ndef throttle(group, item, result):\n return 5 + (2 * random())\n```\n\n#### Caveats regarding exception raising\n\n_Deferred generator usage exception deadlocks_\n\nIf you consume a generator returned by `imap/imap_unordered` somewhere else than where you created it, you may end up in a deadlock if you raise an exception.\n\nThis is not important when using `daemonic=True` but you might stumble upon segfaults on exit because of python reasons beyond my control.\n\n```python\n# Safe\nfor item in imap(...):\n raise RuntimeError\n\n# Not safe\nit = imap(...)\n\nfor item in it:\n raise RuntimeError\n```\n\nIf you really want to do that, because you don't want to use the `ThreadPoolExecutor` context manager, you can try using the experimental `excepthook` kwarg:\n\n```python\n# Probably safe\nit = imap(..., excepthook=True)\n\nfor item in it:\n raise RuntimeError\n```\n\n#### Caveats of using imap with queues\n\n_Typical deadlocks_\n\nEven if `imap` can process an input queue, you should avoid to find yourself in a situation where adding to the queue might block execution if you don't want to end in a deadlock. It can be easy to footgun yourself if your queue has a `maxsize`, for instance:\n\n```python\nfrom queue import Queue\nfrom quenouille import imap\n\njob_queue = Queue(maxsize=2)\njob_queue.put(1)\n\nfor i in imap(job_queue, worker):\n if i < 2:\n job_queue.put(2)\n job_queue.put(3)\n\n # This will put you in a deadlock because this will block\n # because of the queue `maxsize` set to 2\n job_queue.put(4)\n\n print(i)\n```\n\n_Design choices_\n\nTo enable you to add items to the queue in the loop body and so it can safely detect when your queue is drained without race condition, `quenouille` acknowledges that a task is finished only after what you execute in the loop body is done.\n\nThis means that sometimes it might be more performant to only add items to the queue from the worker functions rather than from the loop body.\n\n_queue.task_done_\n\nFor now, `quenouille` does not call `queue.task_done` for you, so this remains your responsability, if you want to be able to call `queue.join` down the line.\n\n\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A library of multithreaded iterator workflows.",
"version": "1.9.1",
"project_urls": {
"Homepage": "http://github.com/medialab/quenouille"
},
"split_keywords": [
"url"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "dfdfd571a2297cf6a8c64e4d27438d5f171de02087d434ab736e16d8e21d3065",
"md5": "0d4fa5219d22588f1e7d01e0db243f30",
"sha256": "61388ef216219e57761392bdc179398aacdce64d8b4f8c121c301ee504769e9b"
},
"downloads": -1,
"filename": "quenouille-1.9.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "0d4fa5219d22588f1e7d01e0db243f30",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.5",
"size": 19196,
"upload_time": "2024-02-23T07:42:38",
"upload_time_iso_8601": "2024-02-23T07:42:38.480175Z",
"url": "https://files.pythonhosted.org/packages/df/df/d571a2297cf6a8c64e4d27438d5f171de02087d434ab736e16d8e21d3065/quenouille-1.9.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "4fba27a48ce300988735ac5768ef54b307d0a90dd6ea5abd74c1941ac5ea6b54",
"md5": "434ee0a2969eb81b5000e5e8f713323c",
"sha256": "6e99690f38fb937e482a84053623ed33a476d17439cac432d91224688c98d820"
},
"downloads": -1,
"filename": "quenouille-1.9.1.tar.gz",
"has_sig": false,
"md5_digest": "434ee0a2969eb81b5000e5e8f713323c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.5",
"size": 21972,
"upload_time": "2024-02-23T07:42:40",
"upload_time_iso_8601": "2024-02-23T07:42:40.571247Z",
"url": "https://files.pythonhosted.org/packages/4f/ba/27a48ce300988735ac5768ef54b307d0a90dd6ea5abd74c1941ac5ea6b54/quenouille-1.9.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-02-23 07:42:40",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "medialab",
"github_project": "quenouille",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [],
"tox": true,
"lcname": "quenouille"
}