···11+# pasturepy
22+33+**pasturepy** is a Python tool for generating JSON feed definitions for use with [Graze](https://graze.social). Use it to programmatically create and customize feeds for Graze.
44+55+## Installation
66+77+```bash
88+pip install pasturepy
99+```
···11+from pasturepy.constants.fields import EMBED_TYPES
22+from pasturepy.constants.graze_json import EMBED_COMPARISONS
33+44+class EmbedNode:
55+66+ @staticmethod
77+ def embed(filter_group, comparison: str, embed_type: str):
88+ """Filter by embeds (images, videos, gifs, links)"""
99+ if embed_type not in EMBED_TYPES:
1010+ raise ValueError(f"Invalid method '{embed_type}'. Must be one of {EMBED_TYPES}")
1111+ if comparison not in EMBED_COMPARISONS:
1212+ raise ValueError(f"Invalid entity_type '{comparison}'. Must be one of {EMBED_COMPARISONS}")
1313+1414+ return filter_group.add_filter({
1515+ "embed_type": [comparison, embed_type]
1616+ })
+16
pasturepy/nodes/entity.py
···11+from pasturepy.constants.fields import ENTITY_TYPES
22+from pasturepy.constants.graze_json import ENTITY_METHODS
33+44+class EntityNode:
55+66+ @staticmethod
77+ def entity(filter_group, method: str, entity_type: str, terms: list):
88+ """Filter by entities (hashtags, mentions, domains, etc.)."""
99+ if method not in ENTITY_METHODS:
1010+ raise ValueError(f"Invalid method '{method}'. Must be one of {ENTITY_METHODS}")
1111+ if entity_type not in ENTITY_TYPES:
1212+ raise ValueError(f"Invalid entity_type '{entity_type}'. Must be one of {ENTITY_TYPES}")
1313+1414+ return filter_group.add_filter({
1515+ method: [entity_type, terms]
1616+ })
+129
pasturepy/nodes/ml.py
···11+from pasturepy.constants.graze_json import COMPARISONS
22+from pasturepy.constants.values import (
33+ CONTENT_MODS, IMAGE_MODS, TOPICS, LANGUAGES,
44+ SENTIMENTS, TOXICITY, EMOTIONS, CATEGORIES, SPAM_TYPE
55+)
66+77+class MLNode:
88+99+ @staticmethod
1010+ def _validate_comparison(comparison: str) -> None:
1111+ if comparison not in COMPARISONS:
1212+ raise ValueError(f"Invalid comparison '{comparison}'. Must be one of {COMPARISONS}")
1313+1414+ @staticmethod
1515+ def _validate_probability(value: float) -> None:
1616+ if not (0 <= value <= 1):
1717+ raise ValueError(f"Probability must be between 0 and 1, got {value}")
1818+ if round(value, 2) != value:
1919+ raise ValueError("Probability can only have up to 2 decimal places")
2020+2121+ @staticmethod
2222+ def content_moderation(filter_group, content_type: str, comparison: str, value: float):
2323+ if content_type not in CONTENT_MODS:
2424+ raise ValueError(f"Invalid content_type. Must be one of {CONTENT_MODS}")
2525+ MLNode._validate_comparison(comparison)
2626+ MLNode._validate_probability(value)
2727+2828+ return filter_group.add_filter({
2929+ "content_moderation": [content_type, comparison, value]
3030+ })
3131+3232+ @staticmethod
3333+ def image_nsfw(filter_group, mod_type: str, comparison: str, value: float):
3434+ if mod_type not in IMAGE_MODS:
3535+ raise ValueError(f"Invalid mod_type. Must be one of {IMAGE_MODS}")
3636+ MLNode._validate_comparison(comparison)
3737+ MLNode._validate_probability(value)
3838+3939+ return filter_group.add_filter({
4040+ "image_nsfw": [mod_type, comparison, value]
4141+ })
4242+4343+ @staticmethod
4444+ def language(filter_group, language: str, comparison: str, value: float):
4545+ if language not in LANGUAGES:
4646+ raise ValueError(f"Invalid language. Must be one of {LANGUAGES}")
4747+ MLNode._validate_comparison(comparison)
4848+ MLNode._validate_probability(value)
4949+5050+ return filter_group.add_filter({
5151+ "language_analysis": [language, comparison, value]
5252+ })
5353+5454+ @staticmethod
5555+ def sentiment(filter_group, sentiment: str, comparison: str, value: float):
5656+ if sentiment not in SENTIMENTS:
5757+ raise ValueError(f"Invalid sentiment. Must be one of {SENTIMENTS}")
5858+ MLNode._validate_comparison(comparison)
5959+ MLNode._validate_probability(value)
6060+6161+ return filter_group.add_filter({
6262+ "sentiment_analysis": [sentiment, comparison, value]
6363+ })
6464+6565+ @staticmethod
6666+ def toxicity(filter_group, toxicity_type: str, comparison: str, value: float):
6767+ if toxicity_type not in TOXICITY:
6868+ raise ValueError(f"Invalid toxicity_type. Must be one of {TOXICITY}")
6969+ MLNode._validate_comparison(comparison)
7070+ MLNode._validate_probability(value)
7171+7272+ return filter_group.add_filter({
7373+ "toxicity_analysis": [toxicity_type, comparison, value]
7474+ })
7575+7676+ @staticmethod
7777+ def topic(filter_group, topic_type: str, comparison: str, value: float):
7878+ if topic_type not in TOPICS:
7979+ raise ValueError(f"Invalid topic_type. Must be one of {TOPICS}")
8080+ MLNode._validate_comparison(comparison)
8181+ MLNode._validate_probability(value)
8282+8383+ return filter_group.add_filter({
8484+ "topic_analysis": [topic_type, comparison, value]
8585+ })
8686+8787+ @staticmethod
8888+ def emotion(filter_group, emotion: str, comparison: str, value: float):
8989+ if emotion not in EMOTIONS:
9090+ raise ValueError(f"Invalid emotion. Must be one of {EMOTIONS}")
9191+ MLNode._validate_comparison(comparison)
9292+ MLNode._validate_probability(value)
9393+9494+ return filter_group.add_filter({
9595+ "topic_analysis": [emotion, comparison, value]
9696+ })
9797+9898+ @staticmethod
9999+ def spam(filter_group, marketing_type: str, comparison: str, value: float):
100100+ if marketing_type not in SPAM_TYPE:
101101+ raise ValueError(f"Invalid marketing_type. Must be one of {SPAM_TYPE}")
102102+ MLNode._validate_comparison(comparison)
103103+ MLNode._validate_probability(value)
104104+105105+ return filter_group.add_filter({
106106+ "marketing_check": [marketing_type, comparison, value]
107107+ })
108108+109109+ @staticmethod
110110+ def img_category(filter_group, img_topic: str, comparison: str, value: float):
111111+ if img_topic not in CATEGORIES:
112112+ raise ValueError(f"Invalid img_topic. Must be one of {CATEGORIES}")
113113+ MLNode._validate_comparison(comparison)
114114+ MLNode._validate_probability(value)
115115+116116+ return filter_group.add_filter({
117117+ "image_arbitary": [img_topic, comparison, value]
118118+ })
119119+120120+ @staticmethod
121121+ def txt_category(filter_group, txt_topic: str, comparison: str, value: float):
122122+ if txt_topic not in CATEGORIES:
123123+ raise ValueError(f"Invalid txt_topic. Must be one of {CATEGORIES}")
124124+ MLNode._validate_comparison(comparison)
125125+ MLNode._validate_probability(value)
126126+127127+ return filter_group.add_filter({
128128+ "text_arbitary": [txt_topic, comparison, value]
129129+ })
+34
pasturepy/nodes/text.py
···11+from pasturepy.constants.fields import TEXT_FIELDS, OPTION_FIELDS
22+from pasturepy.constants.graze_json import REGEX_METHODS, WORD_METHODS
33+44+class TextNode:
55+66+ @staticmethod
77+ def _validate_field(field: str) -> None:
88+ if field not in (TEXT_FIELDS | OPTION_FIELDS):
99+ raise ValueError(
1010+ f"Invalid text field '{field}'. "
1111+ f"Must be one of: {', '.join(sorted(TEXT_FIELDS | OPTION_FIELDS))}"
1212+ )
1313+1414+ @staticmethod
1515+ def word_list(filter_group, method: str, field: str, terms: list,
1616+ ignore_case: bool = True, regex_list: bool = False):
1717+ if method not in WORD_METHODS:
1818+ raise ValueError(f"Invalid method '{method}'. Must be one of {WORD_METHODS}")
1919+ TextNode._validate_field(field)
2020+2121+ return filter_group.add_filter({
2222+ method: [field, terms, ignore_case, regex_list]
2323+ })
2424+2525+ @staticmethod
2626+ def regex(filter_group, method: str, field: str, term: str,
2727+ ignore_case: bool = True):
2828+ if method not in REGEX_METHODS:
2929+ raise ValueError(f"Invalid method '{method}'. Must be one of {REGEX_METHODS}")
3030+ TextNode._validate_field(field)
3131+3232+ return filter_group.add_filter({
3333+ method: [field, term, ignore_case]
3434+ })