mutimon 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mutimon/AI_GUIDE.md +168 -0
- mutimon/__init__.py +3 -0
- mutimon/config.schema.json +637 -0
- mutimon/main.py +1669 -0
- mutimon/skeleton/config.json +192 -0
- mutimon/skeleton/templates/coinmarketcap +13 -0
- mutimon/skeleton/templates/hackernews +14 -0
- mutimon/skeleton/templates/reddit +14 -0
- mutimon-0.1.0.dist-info/METADATA +1104 -0
- mutimon-0.1.0.dist-info/RECORD +14 -0
- mutimon-0.1.0.dist-info/WHEEL +5 -0
- mutimon-0.1.0.dist-info/entry_points.txt +2 -0
- mutimon-0.1.0.dist-info/licenses/LICENSE +21 -0
- mutimon-0.1.0.dist-info/top_level.txt +1 -0
mutimon/AI_GUIDE.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Mutimon — AI Guide for Adding Websites
|
|
2
|
+
|
|
3
|
+
You are configuring Mutimon, a config-driven web scraper. All config lives in `~/.mutimon/config.json`.
|
|
4
|
+
Your job: add a new website to monitor by editing that config file and creating a template.
|
|
5
|
+
|
|
6
|
+
## What you need to do
|
|
7
|
+
|
|
8
|
+
1. Fetch the target page HTML and inspect its structure
|
|
9
|
+
2. Add a **definition** to `defs` (how to scrape)
|
|
10
|
+
3. Add a **rule** to `rules` (when to scrape, who to email)
|
|
11
|
+
4. Create a **Liquid template** file in `~/.mutimon/templates/`
|
|
12
|
+
5. Validate: run `mon --validate`
|
|
13
|
+
6. Test: run `mon --dry-run --force <rule_name>` to verify extraction works
|
|
14
|
+
|
|
15
|
+
## Definition structure (`defs`)
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"my-site": {
|
|
19
|
+
"url": "https://example.com/page",
|
|
20
|
+
"query": {
|
|
21
|
+
"type": "list",
|
|
22
|
+
"selector": "CSS selector for each item container",
|
|
23
|
+
"id": {
|
|
24
|
+
"type": "attribute",
|
|
25
|
+
"name": "data-id"
|
|
26
|
+
},
|
|
27
|
+
"variables": {
|
|
28
|
+
"title": {
|
|
29
|
+
"selector": "h3.title",
|
|
30
|
+
"value": { "type": "text" }
|
|
31
|
+
},
|
|
32
|
+
"url": {
|
|
33
|
+
"selector": "a",
|
|
34
|
+
"value": { "type": "attribute", "name": "href", "prefix": "https://example.com" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Key fields
|
|
42
|
+
|
|
43
|
+
- `url` — page URL, supports `{{param}}` Liquid variables for parametrized URLs
|
|
44
|
+
- `format` — `"html"` (default) or `"xml"` for RSS/Atom feeds
|
|
45
|
+
- `userAgent` — custom User-Agent string (some feeds/APIs block default agents)
|
|
46
|
+
- `query.type` — `"list"` (multiple items) or `"single"` (one value per page)
|
|
47
|
+
- `query.selector` — CSS selector for item containers
|
|
48
|
+
- `query.id` — how to get a unique ID: `{"type": "attribute", "name": "..."}` or `{"source": "url", "regex": "..."}`
|
|
49
|
+
- `query.expect` — array of CSS selectors that must exist (error email if missing, catches page redesigns)
|
|
50
|
+
- `query.reject` — array of CSS selectors that mean "no real results" (returns 0 items if any match)
|
|
51
|
+
|
|
52
|
+
### Variable extraction
|
|
53
|
+
|
|
54
|
+
Each variable needs a `selector` and `value`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
"varname": {
|
|
58
|
+
"selector": "CSS selector relative to item container",
|
|
59
|
+
"value": { "type": "text" }
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Value types:
|
|
64
|
+
- `{"type": "text"}` — text content of the element
|
|
65
|
+
- `{"type": "attribute", "name": "href"}` — attribute value
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
- `"prefix": "https://..."` — prepend to the value (for relative URLs)
|
|
69
|
+
- `"regex": "(\\d+)"` — extract a capture group from the value
|
|
70
|
+
- `"parse": "number"` — parse as float (for numeric comparisons in validators)
|
|
71
|
+
- `"parse": "money"` — locale-aware currency parsing (strips currency symbols)
|
|
72
|
+
- `"default": ""` — fallback when element not found (prevents errors)
|
|
73
|
+
- `"collect": true` — extract ALL matching elements as a list (e.g. tags, skills)
|
|
74
|
+
- `"selector": ":self"` — reference the container element itself
|
|
75
|
+
- `"sibling": true` — search in the next sibling element (e.g. Hacker News score is in a different `<tr>`)
|
|
76
|
+
|
|
77
|
+
## Rule structure (`rules`)
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"ref": "my-site",
|
|
82
|
+
"name": "my-site-monitor",
|
|
83
|
+
"schedule": "0 */6 * * *",
|
|
84
|
+
"subject": "[my-site] {{count}} new item(s)",
|
|
85
|
+
"template": "./templates/my-site",
|
|
86
|
+
"email": "user@example.com"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `ref` — name of the definition in `defs`
|
|
91
|
+
- `name` — unique name, used as state filename
|
|
92
|
+
- `schedule` — cron expression (e.g. `"0 8 * * *"` = daily 8am, `"0 */4 * * *"` = every 4h)
|
|
93
|
+
- `subject` — Liquid template for email subject (`{{count}}` = number of new items)
|
|
94
|
+
- `template` — path to template file relative to `~/.mutimon/`
|
|
95
|
+
- `email` — recipient email address
|
|
96
|
+
- `params` — values for URL template variables (e.g. `{"query": "python"}`)
|
|
97
|
+
|
|
98
|
+
## Template file
|
|
99
|
+
|
|
100
|
+
Create a plain text file in `~/.mutimon/templates/`. Example:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
New items from My Site
|
|
104
|
+
Checked at: {{ now }}
|
|
105
|
+
|
|
106
|
+
Found {{ count }} new item(s)
|
|
107
|
+
============================================================
|
|
108
|
+
{% for item in items %}
|
|
109
|
+
|
|
110
|
+
{{ item.index }}. {{ item.title }}
|
|
111
|
+
URL: {{ item.url }}
|
|
112
|
+
{% endfor %}
|
|
113
|
+
|
|
114
|
+
============================================================
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Available variables: `{{ count }}`, `{{ now }}`, `{{ search_url }}`, and all extracted variables via `{{ item.varname }}`.
|
|
118
|
+
For list variables (from `collect: true`): `{% for tag in item.tags %}{{ tag }}{% unless forloop.last %}, {% endunless %}{% endfor %}`
|
|
119
|
+
|
|
120
|
+
## Complete example: Hacker News
|
|
121
|
+
|
|
122
|
+
Definition:
|
|
123
|
+
```json
|
|
124
|
+
"hackernews": {
|
|
125
|
+
"url": "https://news.ycombinator.com",
|
|
126
|
+
"query": {
|
|
127
|
+
"type": "list",
|
|
128
|
+
"selector": "tr.athing.submission",
|
|
129
|
+
"id": { "type": "attribute", "name": "id" },
|
|
130
|
+
"variables": {
|
|
131
|
+
"title": {
|
|
132
|
+
"selector": ".titleline > a",
|
|
133
|
+
"value": { "type": "text" }
|
|
134
|
+
},
|
|
135
|
+
"url": {
|
|
136
|
+
"selector": ".titleline > a",
|
|
137
|
+
"value": { "type": "attribute", "name": "href" }
|
|
138
|
+
},
|
|
139
|
+
"score": {
|
|
140
|
+
"sibling": true,
|
|
141
|
+
"selector": ".score",
|
|
142
|
+
"value": { "type": "text", "regex": "(\\d+)", "parse": "number" },
|
|
143
|
+
"default": "0"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Rule:
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"ref": "hackernews",
|
|
154
|
+
"name": "hackernews",
|
|
155
|
+
"schedule": "0 */6 * * *",
|
|
156
|
+
"subject": "Hacker News: {{count}} new stories",
|
|
157
|
+
"template": "./templates/hackernews",
|
|
158
|
+
"email": "user@example.com"
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Tips
|
|
163
|
+
|
|
164
|
+
- Always add `"expect"` selectors to detect page redesigns
|
|
165
|
+
- Use `"default": ""` on optional variables to prevent extraction errors
|
|
166
|
+
- Use `mon --dry-run --force <name>` to test without sending emails or saving state
|
|
167
|
+
- For RSS/Atom feeds: use `"format": "xml"` and element names as selectors (e.g. `"item"`, `"entry"`)
|
|
168
|
+
- The `id` field is crucial — it's how Mutimon tracks which items are new
|
mutimon/__init__.py
ADDED