[](https://github.com/iloveitaly/funcy-pipe/releases) [](https://pepy.tech/project/funcy-pipe) [](https://pypi.org/project/funcy-pipe)  [](https://opensource.org/licenses/MIT)
# Funcy with pipeline-based operators
If [Funcy](https://github.com/Suor/funcy) and [Pipe](https://github.com/JulienPalard/Pipe) had a baby. Deal with data transformation in python in a sane way.
I love Ruby. It's a great language and one of the things they got right was pipelined data transformation. Elixir got this
even more right with the explicit pipeline operator `|>`.
However, Python is the way of the future. As I worked more with Python, it was driving me nuts that the
data transformation options were not chainable.
This project fixes this pet peeve.
## Installation
```shell
pip install funcy-pipe
```
Or, if you are using poetry:
```shell
poetry add funcy-pipe
```
## Examples
Extract a couple key values from a sql alchemy model:
```python notest
import funcy_pipe as fp
entities_from_sql_alchemy
| fp.lmap(lambda r: r.to_dict())
| fp.lmap(lambda r: r | fp.omit(["id", "created_at", "updated_at"]))
| fp.to_list
```
Or, you can be more fancy and use [whatever](https://github.com/Suor/whatever) and `pmap`:
```python notest
import funcy_pipe as f
import whatever as _
entities_from_sql_alchemy
| fp.lmap(_.to_dict)
| fp.pmap(fp.omit(["id", "created_at", "updated_at"]))
| fp.to_list
```
Create a map from an array of objects, ensuring the key is always an `int`:
```python notest
section_map = api.get_sections() | fp.group_by(f.compose(int, that.id))
```
Grab the ID of a specific user:
```python notest
filter_user_id = (
collaborator_map().values()
| fp.where(email=target_user)
| fp.pluck("id")
| fp.first()
)
```
Get distinct values from a list (in this case, github events):
```python
events = [
{
"type": "PushEvent"
},
{
"type": "CommentEvent"
}
]
result = events | fp.pluck("type") | fp.distinct() | fp.to_list()
assert ["PushEvent", "CommentEvent"] == result
```
What if the objects are not dicts?
```python notest
filter_user_id = (
collaborator_map().values()
| fp.where_attr(email=target_user)
| fp.pluck_attr("id")
| fp.first()
)
```
How about creating a dict where each value is sorted:
```python notest
data
# each element is a dict of city information, let's group by state
| fp.group_by(itemgetter("state_name"))
# now let's sort each value by population, which is stored as a string
| fp.walk_values(
f.partial(sorted, reverse=True, key=lambda c: int(c["population"])),
)
```
A more complicated example ([lifted from this project](https://github.com/iloveitaly/todoist-digest/blob/2f893709da2cf3a0f715125053af705fc3adbc4c/run.py#L151-L166)):
```python notest
comments = (
# tasks are pulled from the todoist api
tasks
# get all comments for each relevant task
| fp.lmap(lambda task: api.get_comments(task_id=task.id))
# each task's comments are returned as an array, let's flatten this
| fp.flatten()
# dates are returned as strings, let's convert them to datetime objects
| fp.lmap(enrich_date)
# no date filter is applied by default, we don't want all comments
| fp.lfilter(lambda comment: comment["posted_at_date"] > last_synced_date)
# comments do not come with who created the comment by default, we need to hit a separate API to add this to the comment
| fp.lmap(enrich_comment)
# only select the comments posted by our target user
| fp.lfilter(lambda comment: comment["posted_by_user_id"] == filter_user_id)
# there is no `sort` in the funcy library, so we reexport the sort built-in so it's pipe-able
| fp.sort(key="posted_at_date")
# create a dictionary of task_id => [comments]
| fp.group_by(lambda comment: comment["task_id"])
)
```
Want to grab the values of a list of dict keys?
```python
def add_field_name(input: dict, keys: list[str]) -> dict:
return input | {
"field_name": (
keys
# this is a sneaky trick: if we reference the objects method, when it's called it will contain a reference
# to the object
| fp.map(input.get)
| fp.compact
| fp.join_str("_")
)
}
result = [{ "category": "python", "header": "functional"}] | fp.map(fp.rpartial(add_field_name, ["category", "header"])) | fp.to_list
assert result == [{'category': 'python', 'header': 'functional', 'field_name': 'python_functional'}]
```
You can also easily test multiple conditions across API data ([extracted from this project](https://github.com/iloveitaly/github-overlord/blob/a3c0e5d0765b3748747e6721e602c0021be0c8e1/github_overlord/__init__.py#L66-L71))
```python notest
all_checks_successful = (
last_commit.get_check_runs()
| fp.pluck_attr("conclusion")
# if you pass a set into `all` each element of the set is used to build a predicate
# this condition tests if the "conclusion" attribute is either "success" or "skipped"
| fp.all({"success", "skipped"})
)
```
Want to grab the values of a list of dict keys?
```python
def add_field_name(input: dict, keys: list[str]) -> dict:
return input | {
"field_name": (
keys
# this is a sneaky trick: if we reference the objects method, when it's called it will contain a reference
# to the object
| fp.map(input.get)
| fp.compact
| fp.join_str("_")
)
}
result = [{ "category": "python", "header": "functional"}] | fp.map(fp.rpartial(add_field_name, ["category", "header"])) | fp.to_list
assert result == [{'category': 'python', 'header': 'functional', 'field_name': 'python_functional'}]
```
You can also easily group dictionaries by a key (or arbitrary function):
```python
import operator
result = [{"age": 10, "name": "Alice"}, {"age": 12, "name": "Bob"}] | fp.group_by(operator.itemgetter("age"))
assert result == {10: [{'age': 10, 'name': 'Alice'}], 12: [{'age': 12, 'name': 'Bob'}]}
```
## Extras
* to_list
* log
* bp. run `breakpoint()` on the input value
* sort
* exactly_one. Throw an error if the input is not exactly one element
* reduce
* pmap. Pass each element of a sequence into a pipe'd function
## Extensions
There [are some functions](funcy_pipe/funcy_extensions.py) which are not yet merged upstream into funcy, and may never be. You can patch `funcy` to add them using:
```python
import funcy_pipe
funcy_pipe.patch()
```
## Coming From Ruby?
* uniq => distinct
* detect => `where(some="Condition") | first` or `where_attr(some="Condition") | first`
* inverse => complement
* times => repeatedly
### Module Alias
Create a module alias for `funcy-pipe` to make things clean (`import *` always irks me):
```python notest
# fp.py
from funcy_pipe import *
# code py
import fp
```
# Inspiration
* Elixir's pipe operator. `array |> map(fn) |> filter(fn)`
* Ruby's enumerable library. `array.map(&:fn).filter(&:fn)`
* https://pypi.org/project/funcy-chain
* https://github.com/JulienPalard/Pipe
# Related Projects
* https://pydash.readthedocs.io/en/latest/
# TODO
- [ ] tests
- [ ] docs for additional utils
- [ ] fix typing threading
Raw data
{
"_id": null,
"home_page": "https://github.com/iloveitaly/funcy-pipe",
"name": "funcy-pipe",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "python, functional-programming, pipe, funcy, data-manipulation",
"author": "Michael Bianco",
"author_email": "mike@mikebian.co",
"download_url": "https://files.pythonhosted.org/packages/bf/0b/f8472a25c530e2b5aec87654508d19c3a4fc1cd93762497d565736ada0a5/funcy_pipe-0.12.0.tar.gz",
"platform": null,
"description": "[](https://github.com/iloveitaly/funcy-pipe/releases) [](https://pepy.tech/project/funcy-pipe) [](https://pypi.org/project/funcy-pipe)  [](https://opensource.org/licenses/MIT)\n\n# Funcy with pipeline-based operators\n\nIf [Funcy](https://github.com/Suor/funcy) and [Pipe](https://github.com/JulienPalard/Pipe) had a baby. Deal with data transformation in python in a sane way.\n\nI love Ruby. It's a great language and one of the things they got right was pipelined data transformation. Elixir got this\neven more right with the explicit pipeline operator `|>`.\n\nHowever, Python is the way of the future. As I worked more with Python, it was driving me nuts that the\ndata transformation options were not chainable.\n\nThis project fixes this pet peeve.\n\n## Installation\n\n```shell\npip install funcy-pipe\n```\n\nOr, if you are using poetry:\n\n```shell\npoetry add funcy-pipe\n```\n\n## Examples\n\nExtract a couple key values from a sql alchemy model:\n\n```python notest\nimport funcy_pipe as fp\n\nentities_from_sql_alchemy\n | fp.lmap(lambda r: r.to_dict())\n | fp.lmap(lambda r: r | fp.omit([\"id\", \"created_at\", \"updated_at\"]))\n | fp.to_list\n```\n\nOr, you can be more fancy and use [whatever](https://github.com/Suor/whatever) and `pmap`:\n\n```python notest\nimport funcy_pipe as f\nimport whatever as _\n\nentities_from_sql_alchemy\n | fp.lmap(_.to_dict)\n | fp.pmap(fp.omit([\"id\", \"created_at\", \"updated_at\"]))\n | fp.to_list\n```\n\nCreate a map from an array of objects, ensuring the key is always an `int`:\n\n```python notest\nsection_map = api.get_sections() | fp.group_by(f.compose(int, that.id))\n```\n\nGrab the ID of a specific user:\n\n```python notest\nfilter_user_id = (\n collaborator_map().values()\n | fp.where(email=target_user)\n | fp.pluck(\"id\")\n | fp.first()\n)\n```\n\nGet distinct values from a list (in this case, github events):\n\n```python\nevents = [\n {\n \"type\": \"PushEvent\"\n },\n {\n \"type\": \"CommentEvent\"\n }\n]\n\nresult = events | fp.pluck(\"type\") | fp.distinct() | fp.to_list()\n\nassert [\"PushEvent\", \"CommentEvent\"] == result\n```\n\nWhat if the objects are not dicts?\n\n```python notest\nfilter_user_id = (\n collaborator_map().values()\n | fp.where_attr(email=target_user)\n | fp.pluck_attr(\"id\")\n | fp.first()\n)\n```\n\nHow about creating a dict where each value is sorted:\n\n```python notest\ndata\n # each element is a dict of city information, let's group by state\n | fp.group_by(itemgetter(\"state_name\"))\n # now let's sort each value by population, which is stored as a string\n | fp.walk_values(\n f.partial(sorted, reverse=True, key=lambda c: int(c[\"population\"])),\n )\n```\n\nA more complicated example ([lifted from this project](https://github.com/iloveitaly/todoist-digest/blob/2f893709da2cf3a0f715125053af705fc3adbc4c/run.py#L151-L166)):\n\n```python notest\ncomments = (\n # tasks are pulled from the todoist api\n tasks\n # get all comments for each relevant task\n | fp.lmap(lambda task: api.get_comments(task_id=task.id))\n # each task's comments are returned as an array, let's flatten this\n | fp.flatten()\n # dates are returned as strings, let's convert them to datetime objects\n | fp.lmap(enrich_date)\n # no date filter is applied by default, we don't want all comments\n | fp.lfilter(lambda comment: comment[\"posted_at_date\"] > last_synced_date)\n # comments do not come with who created the comment by default, we need to hit a separate API to add this to the comment\n | fp.lmap(enrich_comment)\n # only select the comments posted by our target user\n | fp.lfilter(lambda comment: comment[\"posted_by_user_id\"] == filter_user_id)\n # there is no `sort` in the funcy library, so we reexport the sort built-in so it's pipe-able\n | fp.sort(key=\"posted_at_date\")\n # create a dictionary of task_id => [comments]\n | fp.group_by(lambda comment: comment[\"task_id\"])\n)\n```\n\nWant to grab the values of a list of dict keys?\n\n```python\ndef add_field_name(input: dict, keys: list[str]) -> dict:\n return input | {\n \"field_name\": (\n keys\n # this is a sneaky trick: if we reference the objects method, when it's called it will contain a reference\n # to the object\n | fp.map(input.get)\n | fp.compact\n | fp.join_str(\"_\")\n )\n }\n\nresult = [{ \"category\": \"python\", \"header\": \"functional\"}] | fp.map(fp.rpartial(add_field_name, [\"category\", \"header\"])) | fp.to_list\nassert result == [{'category': 'python', 'header': 'functional', 'field_name': 'python_functional'}]\n```\n\nYou can also easily test multiple conditions across API data ([extracted from this project](https://github.com/iloveitaly/github-overlord/blob/a3c0e5d0765b3748747e6721e602c0021be0c8e1/github_overlord/__init__.py#L66-L71))\n\n```python notest\nall_checks_successful = (\n last_commit.get_check_runs()\n | fp.pluck_attr(\"conclusion\")\n # if you pass a set into `all` each element of the set is used to build a predicate\n # this condition tests if the \"conclusion\" attribute is either \"success\" or \"skipped\"\n | fp.all({\"success\", \"skipped\"})\n)\n```\n\nWant to grab the values of a list of dict keys?\n\n```python\ndef add_field_name(input: dict, keys: list[str]) -> dict:\n return input | {\n \"field_name\": (\n keys\n # this is a sneaky trick: if we reference the objects method, when it's called it will contain a reference\n # to the object\n | fp.map(input.get)\n | fp.compact\n | fp.join_str(\"_\")\n )\n }\n\nresult = [{ \"category\": \"python\", \"header\": \"functional\"}] | fp.map(fp.rpartial(add_field_name, [\"category\", \"header\"])) | fp.to_list\nassert result == [{'category': 'python', 'header': 'functional', 'field_name': 'python_functional'}]\n```\n\nYou can also easily group dictionaries by a key (or arbitrary function):\n\n```python\nimport operator\n\nresult = [{\"age\": 10, \"name\": \"Alice\"}, {\"age\": 12, \"name\": \"Bob\"}] | fp.group_by(operator.itemgetter(\"age\"))\nassert result == {10: [{'age': 10, 'name': 'Alice'}], 12: [{'age': 12, 'name': 'Bob'}]}\n```\n\n## Extras\n\n* to_list\n* log\n* bp. run `breakpoint()` on the input value\n* sort\n* exactly_one. Throw an error if the input is not exactly one element\n* reduce\n* pmap. Pass each element of a sequence into a pipe'd function\n\n## Extensions\n\nThere [are some functions](funcy_pipe/funcy_extensions.py) which are not yet merged upstream into funcy, and may never be. You can patch `funcy` to add them using:\n\n```python\nimport funcy_pipe\nfuncy_pipe.patch()\n```\n\n## Coming From Ruby?\n\n* uniq => distinct\n* detect => `where(some=\"Condition\") | first` or `where_attr(some=\"Condition\") | first`\n* inverse => complement\n* times => repeatedly\n\n### Module Alias\n\nCreate a module alias for `funcy-pipe` to make things clean (`import *` always irks me):\n\n```python notest\n# fp.py\nfrom funcy_pipe import *\n\n# code py\nimport fp\n```\n\n# Inspiration\n\n* Elixir's pipe operator. `array |> map(fn) |> filter(fn)`\n* Ruby's enumerable library. `array.map(&:fn).filter(&:fn)`\n* https://pypi.org/project/funcy-chain\n* https://github.com/JulienPalard/Pipe\n\n# Related Projects\n\n* https://pydash.readthedocs.io/en/latest/\n\n# TODO\n\n- [ ] tests\n- [ ] docs for additional utils\n- [ ] fix typing threading\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "If Funcy and Pipe had a baby. Decorates all Funcy methods with Pipe superpowers.",
"version": "0.12.0",
"project_urls": {
"Homepage": "https://github.com/iloveitaly/funcy-pipe"
},
"split_keywords": [
"python",
" functional-programming",
" pipe",
" funcy",
" data-manipulation"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "46fc4d54374a988b8619d950cd32cc1cfd3c07a4af85811fea1b68ecb40fdcd3",
"md5": "8c9b0b980ca0b511211d61c7d07d8c4c",
"sha256": "7afe5e07702352cbcfe4ea60ae13a1540c84e2d14f58b3997fecde13d189db6a"
},
"downloads": -1,
"filename": "funcy_pipe-0.12.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "8c9b0b980ca0b511211d61c7d07d8c4c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 10851,
"upload_time": "2025-09-10T16:41:07",
"upload_time_iso_8601": "2025-09-10T16:41:07.064017Z",
"url": "https://files.pythonhosted.org/packages/46/fc/4d54374a988b8619d950cd32cc1cfd3c07a4af85811fea1b68ecb40fdcd3/funcy_pipe-0.12.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "bf0bf8472a25c530e2b5aec87654508d19c3a4fc1cd93762497d565736ada0a5",
"md5": "cad11240072ae2b0453a06997d5f6d20",
"sha256": "418a200c7fec3caf2ca5204b78c0009e26ee157724d14ef48d210acb49a28655"
},
"downloads": -1,
"filename": "funcy_pipe-0.12.0.tar.gz",
"has_sig": false,
"md5_digest": "cad11240072ae2b0453a06997d5f6d20",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 11759,
"upload_time": "2025-09-10T16:41:07",
"upload_time_iso_8601": "2025-09-10T16:41:07.861840Z",
"url": "https://files.pythonhosted.org/packages/bf/0b/f8472a25c530e2b5aec87654508d19c3a4fc1cd93762497d565736ada0a5/funcy_pipe-0.12.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-09-10 16:41:07",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "iloveitaly",
"github_project": "funcy-pipe",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "funcy-pipe"
}