riko 0.68.1__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.
- riko/.DS_Store +0 -0
- riko/__init__.py +321 -0
- riko/_pubsub/.DS_Store +0 -0
- riko/autorss.py +89 -0
- riko/bado/.DS_Store +0 -0
- riko/bado/__init__.py +36 -0
- riko/bado/io.py +150 -0
- riko/bado/itertools.py +102 -0
- riko/bado/microdom.py +1113 -0
- riko/bado/mock.py +74 -0
- riko/bado/requests.py +22 -0
- riko/bado/sux.py +680 -0
- riko/bado/util.py +93 -0
- riko/cast.py +386 -0
- riko/cli/.DS_Store +0 -0
- riko/cli/__init__.py +0 -0
- riko/cli/benchmark.py +173 -0
- riko/cli/manage.py +255 -0
- riko/cli/runpipe.py +79 -0
- riko/collections.py +642 -0
- riko/compile.py +358 -0
- riko/currencies.py +1208 -0
- riko/data/.DS_Store +0 -0
- riko/data/Pinterest.html +3617 -0
- riko/data/Politik.xml +260 -0
- riko/data/TheEdTechie.xml +423 -0
- riko/data/Topthemen.xml +249 -0
- riko/data/api.github.com_search_users?q=defunkt.json +622 -0
- riko/data/autoblog.xml +1312 -0
- riko/data/bbc.html +1301 -0
- riko/data/bbci.co.uk.xml +323 -0
- riko/data/blog.ouseful.info_feed.xml +502 -0
- riko/data/caltrain.html +566 -0
- riko/data/capnorth.oes.ca.gov_89509.xml +73 -0
- riko/data/capnorth.xml +73 -0
- riko/data/cnn.html +1771 -0
- riko/data/countries.csv +260 -0
- riko/data/currencies.csv +167 -0
- riko/data/currencies.json +1064 -0
- riko/data/delicious.xml +328 -0
- riko/data/edition.cnn.html +1100 -0
- riko/data/elance.json +1730 -0
- riko/data/election_results.json +25822 -0
- riko/data/feed.xml +217 -0
- riko/data/feeds.delicious.com_v2_rss_popular?count=15.xml +328 -0
- riko/data/feeds.delicious.com_v2_rss_popular?count=3.xml +84 -0
- riko/data/feeds.delicious.com_v2_rss_psychemedia?count=15.xml +267 -0
- riko/data/feeds.feedburner.com_TheEdTechie.xml +313 -0
- riko/data/feeds.feedburner.com_ouseful.xml +546 -0
- riko/data/feeds.feedburner.com_yodelanecdotal.xml +97 -0
- riko/data/feeds.gawker.com_jalopnik_full.xml +2 -0
- riko/data/fourtitude.xml +120 -0
- riko/data/freelancer.json +465 -0
- riko/data/gawker.xml +308 -0
- riko/data/gigs.json +1336 -0
- riko/data/greenhughes.xml +109 -0
- riko/data/guru.json +1729 -0
- riko/data/health.xml +413 -0
- riko/data/kazeeki.json +4135 -0
- riko/data/kazeeki2.json +119 -0
- riko/data/kazeeki3.json +27 -0
- riko/data/lorem.txt +32 -0
- riko/data/news.google.com_rss?hl=en-US&gl=US&ceid=US_3Aen&q=enterprise+mashup.xml +524 -0
- riko/data/news.yahoo.com_rss_health.xml +414 -0
- riko/data/news.yahoo.com_rss_topstories.html +1343 -0
- riko/data/news.yahoo.com_rss_topstories.xml +443 -0
- riko/data/odesk.json +224 -0
- riko/data/ouseful.xml +580 -0
- riko/data/ouseful_feedburner.xml +673 -0
- riko/data/places.xml +31 -0
- riko/data/podcast.xml +661 -0
- riko/data/psychemedia_delicious.xml +261 -0
- riko/data/psychemedia_slideshare.xml +891 -0
- riko/data/quote.json +178 -0
- riko/data/rsc-ne-scotland.org.uk_mashe_ititle_xml_jisc10bean.xml +390 -0
- riko/data/rss.sueddeutsche.de_rss_Politik.xml +215 -0
- riko/data/rss.sueddeutsche.de_rss_Topthemen.xml +204 -0
- riko/data/schools.xml +20 -0
- riko/data/sciencedaily.html +873 -0
- riko/data/scotland.xml +391 -0
- riko/data/spreadsheet.csv +646 -0
- riko/data/spreadsheets.google.com_pub?key=p1rHUqg4g420UMaN1sPvaRg&output=csv&range=a1_AB646.csv +646 -0
- riko/data/status.csv +1 -0
- riko/data/topstories.xml +443 -0
- riko/data/users.jyu.fi.html +45 -0
- riko/data/users.jyu.fi_~atsoukka_cgi_bin_aarresaari.html +37 -0
- riko/data/www.autoblog.com_rss.xml +435 -0
- riko/data/www.bbc.co.uk_news.html +3251 -0
- riko/data/www.bbc.co.uk_programmes_b006mvlc_episodes_player.xml +661 -0
- riko/data/www.caltrain.com_Fares_farechart.html +527 -0
- riko/data/www.fourtitude.com_news_publish_rss.xml +313 -0
- riko/data/www.greenhughes.com_rssfeed.xml +238 -0
- riko/data/www.sciencedaily.com_rss_computers_math.html +811 -0
- riko/data/www.slideshare.net_rss_user_psychemedia.xml +891 -0
- riko/data/www.theguardian.com_politics_api_general_election_2010_results_json.json +1 -0
- riko/data/yodel.xml +447 -0
- riko/data/yql.xml +21 -0
- riko/dates.py +145 -0
- riko/dotdict.py +423 -0
- riko/helpers.py +105 -0
- riko/locations.py +1769 -0
- riko/modules/.DS_Store +0 -0
- riko/modules/__init__.py +971 -0
- riko/modules/aggregate.py +120 -0
- riko/modules/count.py +173 -0
- riko/modules/csv.py +239 -0
- riko/modules/currencyformat.py +147 -0
- riko/modules/datebuilder.py +148 -0
- riko/modules/dateformat.py +156 -0
- riko/modules/exchangerate.py +318 -0
- riko/modules/feedautodiscovery.py +212 -0
- riko/modules/fetch.py +214 -0
- riko/modules/fetchdata.py +223 -0
- riko/modules/fetchpage.py +235 -0
- riko/modules/fetchsitefeed.py +203 -0
- riko/modules/fetchtable.py +239 -0
- riko/modules/fetchtext.py +204 -0
- riko/modules/filter.py +290 -0
- riko/modules/forever.py +135 -0
- riko/modules/geolocate.py +155 -0
- riko/modules/hash.py +136 -0
- riko/modules/input.py +226 -0
- riko/modules/itembuilder.py +150 -0
- riko/modules/join.py +195 -0
- riko/modules/loop.py +103 -0
- riko/modules/numberinput.py +20 -0
- riko/modules/receive.py +176 -0
- riko/modules/refind.py +257 -0
- riko/modules/regex.py +311 -0
- riko/modules/rename.py +231 -0
- riko/modules/reverse.py +115 -0
- riko/modules/rssitembuilder.py +194 -0
- riko/modules/send.py +124 -0
- riko/modules/simplemath.py +168 -0
- riko/modules/slugify.py +129 -0
- riko/modules/sort.py +224 -0
- riko/modules/split.py +143 -0
- riko/modules/strconcat.py +151 -0
- riko/modules/strfind.py +250 -0
- riko/modules/strreplace.py +222 -0
- riko/modules/strtransform.py +240 -0
- riko/modules/subelement.py +200 -0
- riko/modules/substr.py +149 -0
- riko/modules/sum.py +185 -0
- riko/modules/tail.py +131 -0
- riko/modules/timeout.py +219 -0
- riko/modules/tokenizer.py +167 -0
- riko/modules/truncate.py +141 -0
- riko/modules/typecast.py +154 -0
- riko/modules/udf.py +119 -0
- riko/modules/union.py +139 -0
- riko/modules/uniq.py +158 -0
- riko/modules/urlbuilder.py +212 -0
- riko/modules/urlinput.py +21 -0
- riko/modules/urlparse.py +137 -0
- riko/modules/xpathfetchpage.py +287 -0
- riko/parsers.py +540 -0
- riko/pprint2.py +131 -0
- riko/templates/pypipe.txt +35 -0
- riko/topsort.py +163 -0
- riko/types/.DS_Store +0 -0
- riko/types/compile.py +113 -0
- riko/types/general.py +354 -0
- riko/types/modules.py +106 -0
- riko/utils.py +897 -0
- riko-0.68.1.dist-info/METADATA +756 -0
- riko-0.68.1.dist-info/RECORD +170 -0
- riko-0.68.1.dist-info/WHEEL +4 -0
- riko-0.68.1.dist-info/entry_points.txt +5 -0
- riko-0.68.1.dist-info/licenses/LICENSE +20 -0
riko/.DS_Store
ADDED
|
Binary file
|
riko/__init__.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# vim: sw=4:ts=4:expandtab
|
|
2
|
+
"""
|
|
3
|
+
riko
|
|
4
|
+
~~~~
|
|
5
|
+
Provides functions for analyzing and processing streams of structured data
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
basic usage::
|
|
9
|
+
|
|
10
|
+
>>> from riko.modules.itembuilder import pipe as itembuilder
|
|
11
|
+
>>> from riko.modules.strreplace import pipe as strreplace
|
|
12
|
+
>>> from riko.collections import SyncPipe
|
|
13
|
+
>>>
|
|
14
|
+
>>> ib_conf = {
|
|
15
|
+
... 'attrs': [
|
|
16
|
+
... {'key': 'link', 'value': 'www.google.com'},
|
|
17
|
+
... {'key': 'title', 'value': 'google'},
|
|
18
|
+
... {'key': 'author', 'value': 'Tommy'}
|
|
19
|
+
... ]
|
|
20
|
+
... }
|
|
21
|
+
>>>
|
|
22
|
+
>>> items = itembuilder(conf=ib_conf)
|
|
23
|
+
>>> next(items)
|
|
24
|
+
{'link': 'www.google.com', 'title': 'google', 'author': 'Tommy'}
|
|
25
|
+
>>> sr_conf = {
|
|
26
|
+
... 'rule': [{'find': 'Tom', 'param': 'first', 'replace': 'Tim'}]
|
|
27
|
+
... }
|
|
28
|
+
>>>
|
|
29
|
+
>>> items = itembuilder(conf=ib_conf)
|
|
30
|
+
>>> replaced = strreplace(next(items), conf=sr_conf, field='author')
|
|
31
|
+
>>> next(replaced)['strreplace']
|
|
32
|
+
'Timmy'
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence
|
|
37
|
+
from importlib.metadata import metadata, version
|
|
38
|
+
from os import path as p
|
|
39
|
+
from time import struct_time
|
|
40
|
+
from typing import (
|
|
41
|
+
Any,
|
|
42
|
+
Literal,
|
|
43
|
+
TypeVar,
|
|
44
|
+
overload,
|
|
45
|
+
)
|
|
46
|
+
from typing import cast as cast_type
|
|
47
|
+
|
|
48
|
+
from meza import compat
|
|
49
|
+
from meza.fntools import Objectify as _Objectify
|
|
50
|
+
|
|
51
|
+
from riko.types.general import (
|
|
52
|
+
BasicArg,
|
|
53
|
+
ComplexArg,
|
|
54
|
+
ComplexMapping,
|
|
55
|
+
IntermediateValue,
|
|
56
|
+
ObjconfParam,
|
|
57
|
+
ObjconfRule,
|
|
58
|
+
SyncItemFunc,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# https://github.com/astral-sh/uv/issues/7533#issuecomment-2472804995
|
|
62
|
+
meta = metadata("riko")
|
|
63
|
+
|
|
64
|
+
PACKAGE_INFO = {
|
|
65
|
+
"__version__": version("riko"),
|
|
66
|
+
"__title__": meta["Name"],
|
|
67
|
+
"__package_name__": meta["Name"],
|
|
68
|
+
"__description__": meta.get("Summary") or meta.get("Description", ""),
|
|
69
|
+
"__license__": meta.get("License-Expression") or meta.get("License", ""),
|
|
70
|
+
"__author__": meta.get("Author", ""),
|
|
71
|
+
"__email__": meta.get("Author-email", ""),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __getattr__(name: str) -> str:
|
|
76
|
+
if name in PACKAGE_INFO:
|
|
77
|
+
return PACKAGE_INFO[name]
|
|
78
|
+
else:
|
|
79
|
+
msg = f"module {__name__} has no attribute {name}"
|
|
80
|
+
raise AttributeError(msg)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__copyright__ = "Copyright 2015 Reuben Cummings"
|
|
84
|
+
|
|
85
|
+
PARENT_DIR = p.abspath(p.dirname(__file__))
|
|
86
|
+
ENCODING = "utf-8"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_path(name: str):
|
|
90
|
+
if name.startswith(("http", "file:")):
|
|
91
|
+
url = name
|
|
92
|
+
else:
|
|
93
|
+
url = f"file://{p.join(PARENT_DIR, 'data', name)}"
|
|
94
|
+
|
|
95
|
+
return url
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_abspath(url: str, offline=False):
|
|
99
|
+
if url.startswith(("http", "file:///")):
|
|
100
|
+
pass
|
|
101
|
+
elif url.startswith("file://"):
|
|
102
|
+
parent = p.dirname(p.dirname(__file__))
|
|
103
|
+
rel_path = url[7:]
|
|
104
|
+
abspath = p.abspath(p.join(parent, rel_path))
|
|
105
|
+
url = f"file://{abspath}"
|
|
106
|
+
elif offline:
|
|
107
|
+
url = get_path(url)
|
|
108
|
+
else:
|
|
109
|
+
url = f"http://{url}" if url and "://" not in url else url
|
|
110
|
+
|
|
111
|
+
return compat.decode(url)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def replacer(content: str, old: str, new="_") -> str:
|
|
115
|
+
if old:
|
|
116
|
+
replaced = content.replace(old, new)
|
|
117
|
+
elif content[0].isdecimal() or not content[0].isascii():
|
|
118
|
+
replaced = f"{new}{content}"
|
|
119
|
+
else:
|
|
120
|
+
replaced = content
|
|
121
|
+
|
|
122
|
+
return replaced
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Context:
|
|
126
|
+
"""
|
|
127
|
+
The context of a pipeline
|
|
128
|
+
verbose = debug printing during compilation and running
|
|
129
|
+
describe_input = return pipe input requirements
|
|
130
|
+
describe_dependencies = return a list of sub-pipelines used
|
|
131
|
+
test = takes input values from default (skips the console prompt)
|
|
132
|
+
inputs = a dictionary of values that overrides the defaults
|
|
133
|
+
e.g. {'name one': 'test value1'}
|
|
134
|
+
submodule = takes input values from inputs (or default)
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, **kwargs):
|
|
138
|
+
self.verbose = kwargs.get("verbose", False)
|
|
139
|
+
self.test = kwargs.get("test", False)
|
|
140
|
+
self.describe_input = kwargs.get("describe_input", False)
|
|
141
|
+
self.describe_dependencies = kwargs.get("describe_dependencies", False)
|
|
142
|
+
self.inputs = kwargs.get("inputs", {})
|
|
143
|
+
self.submodule = kwargs.get("submodule", False)
|
|
144
|
+
|
|
145
|
+
def __repr__(self):
|
|
146
|
+
content = f"verbose={self.verbose}, test={self.test}, "
|
|
147
|
+
content += f"describe_input={self.describe_input}, "
|
|
148
|
+
content = f"describe_dependencies={self.describe_dependencies}, "
|
|
149
|
+
content = f"inputs={self.inputs}, submodule={self.submodule}"
|
|
150
|
+
return f"Context({content})"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class Objectify(_Objectify):
|
|
154
|
+
"""
|
|
155
|
+
Creates an object with dynamically set attributes. Useful
|
|
156
|
+
for accessing the kwargs of a function as attributes.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, data: ComplexMapping, *args, **kwargs):
|
|
160
|
+
"""
|
|
161
|
+
Objectify constructor
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
data (dict): The attributes to set
|
|
165
|
+
defaults (dict): The default attributes
|
|
166
|
+
|
|
167
|
+
Examples:
|
|
168
|
+
>>> kw = Objectify({'KEY': 'foo'})
|
|
169
|
+
>>> kw.key
|
|
170
|
+
'foo'
|
|
171
|
+
>>> kw['key']
|
|
172
|
+
'foo'
|
|
173
|
+
>>> kw.get('key')
|
|
174
|
+
'foo'
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
_data = {k.lower(): v for k, v in data.items()}
|
|
178
|
+
super().__init__(_data, *args, **kwargs)
|
|
179
|
+
|
|
180
|
+
def __getattribute__(self, name: str) -> BasicArg:
|
|
181
|
+
return super().__getattribute__(name)
|
|
182
|
+
|
|
183
|
+
def __getitem__(self, name: str) -> BasicArg:
|
|
184
|
+
item = super().__getitem__(name)
|
|
185
|
+
return cast_type(BasicArg, item)
|
|
186
|
+
|
|
187
|
+
def __iter__(self) -> Iterator[str]:
|
|
188
|
+
return super().__iter__()
|
|
189
|
+
|
|
190
|
+
def iteritems(self) -> Iterator[tuple[str, BasicArg]]:
|
|
191
|
+
return super().iteritems()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Objconf(Objectify):
|
|
195
|
+
assign: str
|
|
196
|
+
attrs: Sequence[str]
|
|
197
|
+
base: str
|
|
198
|
+
col_names: Sequence[str]
|
|
199
|
+
combine: Literal["and", "or"]
|
|
200
|
+
count: str
|
|
201
|
+
currency: str
|
|
202
|
+
debug: bool
|
|
203
|
+
default: BasicArg
|
|
204
|
+
delay: int
|
|
205
|
+
delimiter: str
|
|
206
|
+
detag: bool
|
|
207
|
+
emit: bool
|
|
208
|
+
encoding: str
|
|
209
|
+
end: str
|
|
210
|
+
ext: str
|
|
211
|
+
format: str
|
|
212
|
+
group_key: str
|
|
213
|
+
html5: bool
|
|
214
|
+
join_key: str
|
|
215
|
+
length: int
|
|
216
|
+
limit: int
|
|
217
|
+
lower: bool
|
|
218
|
+
max_len: int
|
|
219
|
+
max_wait: int
|
|
220
|
+
multi: bool
|
|
221
|
+
name: str
|
|
222
|
+
other: str
|
|
223
|
+
op: str
|
|
224
|
+
other_join_key: str
|
|
225
|
+
param: ObjconfParam | Sequence[ObjconfParam]
|
|
226
|
+
parse_key: str
|
|
227
|
+
part: str
|
|
228
|
+
path: str | list[str]
|
|
229
|
+
permit: bool
|
|
230
|
+
precision: int
|
|
231
|
+
prompt: str
|
|
232
|
+
rule: ObjconfRule | Sequence[ObjconfRule]
|
|
233
|
+
skip_rows: int
|
|
234
|
+
sort: int
|
|
235
|
+
start: int
|
|
236
|
+
stop: str
|
|
237
|
+
strict: bool
|
|
238
|
+
stringify: bool
|
|
239
|
+
sum_key: str
|
|
240
|
+
times: int
|
|
241
|
+
token: str
|
|
242
|
+
token_key: str
|
|
243
|
+
type: str
|
|
244
|
+
unique_key: str
|
|
245
|
+
url: str
|
|
246
|
+
wait: int
|
|
247
|
+
xpath: str
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def objectify(
|
|
251
|
+
data: ComplexArg, func: SyncItemFunc | None = None, **defaults
|
|
252
|
+
) -> ComplexArg:
|
|
253
|
+
if isinstance(data, Mapping):
|
|
254
|
+
objectified = Objectify(data, func=func, **defaults)
|
|
255
|
+
elif func:
|
|
256
|
+
if isinstance(data, (str, struct_time)):
|
|
257
|
+
objectified = func(data)
|
|
258
|
+
elif isinstance(data, Sequence):
|
|
259
|
+
objectified = [objectify(d, func) for d in data]
|
|
260
|
+
else:
|
|
261
|
+
objectified = func(data)
|
|
262
|
+
else:
|
|
263
|
+
objectified = data
|
|
264
|
+
|
|
265
|
+
return objectified
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# TODO: move back to meza
|
|
269
|
+
K = TypeVar("K")
|
|
270
|
+
T = TypeVar("T")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@overload
|
|
274
|
+
def listize(value: Mapping[K, T]) -> list[Mapping[K, T]]: ...
|
|
275
|
+
@overload
|
|
276
|
+
def listize(value: Callable[..., T]) -> list[Callable[..., T]]: ...
|
|
277
|
+
@overload
|
|
278
|
+
def listize(value: IntermediateValue) -> list[IntermediateValue]: ...
|
|
279
|
+
@overload
|
|
280
|
+
def listize(value: list[T]) -> list[T]: ...
|
|
281
|
+
@overload
|
|
282
|
+
def listize(value: Sequence[T]) -> Sequence[T]: ...
|
|
283
|
+
@overload
|
|
284
|
+
def listize(value: Generator[T, None, None]) -> Generator[T, None, None]: ...
|
|
285
|
+
@overload
|
|
286
|
+
def listize(value: Iterator[T]) -> Iterator[T]: ...
|
|
287
|
+
@overload
|
|
288
|
+
def listize(value: Iterable[T]) -> Iterable[T]: ...
|
|
289
|
+
def listize(
|
|
290
|
+
value: Any,
|
|
291
|
+
) -> list[Mapping | IntermediateValue | Callable] | Iterable | Sequence:
|
|
292
|
+
"""
|
|
293
|
+
Create a listlike object from any value
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
value: The object to convert
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
value as a listlike object (wrapped in a list or the value itself)
|
|
300
|
+
|
|
301
|
+
Examples:
|
|
302
|
+
>>> listize(x for x in range(3)) # doctest: +ELLIPSIS
|
|
303
|
+
<generator object <genexpr> at 0x...>
|
|
304
|
+
>>> listize([x for x in range(3)])
|
|
305
|
+
[0, 1, 2]
|
|
306
|
+
>>> listize(iter(x for x in range(3))) # doctest: +ELLIPSIS
|
|
307
|
+
<generator object <genexpr> at 0x...>
|
|
308
|
+
>>> listize(range(3))
|
|
309
|
+
range(0, 3)
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
if not value:
|
|
313
|
+
result = []
|
|
314
|
+
elif isinstance(value, (Mapping, IntermediateValue, Callable)):
|
|
315
|
+
result = [value]
|
|
316
|
+
elif isinstance(value, (Iterable, Sequence)):
|
|
317
|
+
result = value
|
|
318
|
+
else:
|
|
319
|
+
result = [value]
|
|
320
|
+
|
|
321
|
+
return result
|
riko/_pubsub/.DS_Store
ADDED
|
Binary file
|
riko/autorss.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# vim: sw=4:ts=4:expandtab
|
|
2
|
+
"""
|
|
3
|
+
riko.autorss
|
|
4
|
+
~~~~~~~~~~~~
|
|
5
|
+
Provides functions for finding RSS feeds from a site's LINK tags
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Generator, Iterator
|
|
9
|
+
from io import StringIO, TextIOBase
|
|
10
|
+
|
|
11
|
+
import pygogo as gogo
|
|
12
|
+
from meza.compat import decode
|
|
13
|
+
from twisted.internet.defer import Deferred
|
|
14
|
+
|
|
15
|
+
from riko.bado import coroutine, microdom, return_value
|
|
16
|
+
from riko.bado.io import async_url_open
|
|
17
|
+
from riko.parsers import LinkParser
|
|
18
|
+
from riko.utils import Fetch
|
|
19
|
+
|
|
20
|
+
TIMEOUT = 10
|
|
21
|
+
logger = gogo.Gogo(__name__, monolog=True).logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RSSLinkParser(LinkParser):
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, rss_only=True, **kwargs)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def file2entries(
|
|
30
|
+
f: StringIO | Iterator[str] | TextIOBase, parser: RSSLinkParser
|
|
31
|
+
) -> Iterator[dict]:
|
|
32
|
+
for line in f:
|
|
33
|
+
parser.feed(decode(line))
|
|
34
|
+
|
|
35
|
+
yield from parser.entry
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def doc2entries(document) -> Iterator[dict]:
|
|
39
|
+
for node in document.childNodes:
|
|
40
|
+
if hasattr(node, "attributes") and node.attributes:
|
|
41
|
+
entry = node.attributes
|
|
42
|
+
alternate = entry.get("rel") == "alternate"
|
|
43
|
+
rss = "rss" in entry.get("type", "")
|
|
44
|
+
else:
|
|
45
|
+
alternate = rss = None
|
|
46
|
+
entry = {}
|
|
47
|
+
|
|
48
|
+
if (alternate or rss) and "href" in entry:
|
|
49
|
+
entry["link"] = entry["href"]
|
|
50
|
+
entry["tag"] = node.nodeName
|
|
51
|
+
yield entry
|
|
52
|
+
|
|
53
|
+
for node in document.childNodes:
|
|
54
|
+
for entry in doc2entries(node):
|
|
55
|
+
yield entry
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@coroutine # pyright: ignore[reportArgumentType]
|
|
59
|
+
def async_get_rss(
|
|
60
|
+
url: str, **kwargs
|
|
61
|
+
) -> Generator[Deferred[Iterator[dict]], Iterator[dict], None]:
|
|
62
|
+
try:
|
|
63
|
+
f = yield async_url_open(url, timeout=TIMEOUT) # pyright: ignore[reportCallIssue]
|
|
64
|
+
except ValueError:
|
|
65
|
+
f = filter(None, url.splitlines())
|
|
66
|
+
|
|
67
|
+
document = microdom.parse(f, lenient=True)
|
|
68
|
+
return_value(doc2entries(document))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_rss(
|
|
72
|
+
url: str, convert_charrefs=False, auto_sort=False, **kwargs
|
|
73
|
+
) -> Iterator[dict]:
|
|
74
|
+
try:
|
|
75
|
+
parser = RSSLinkParser(convert_charrefs=convert_charrefs, **kwargs)
|
|
76
|
+
except TypeError:
|
|
77
|
+
parser = RSSLinkParser(**kwargs)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
f = Fetch(url, timeout=TIMEOUT)
|
|
81
|
+
except ValueError:
|
|
82
|
+
f = filter(None, url.splitlines())
|
|
83
|
+
|
|
84
|
+
entries = file2entries(f, parser)
|
|
85
|
+
|
|
86
|
+
if auto_sort:
|
|
87
|
+
entries = iter(sorted(entries, key=parser.keyfunc))
|
|
88
|
+
|
|
89
|
+
return entries
|
riko/bado/.DS_Store
ADDED
|
Binary file
|
riko/bado/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# vim: sw=4:ts=4:expandtab
|
|
2
|
+
"""
|
|
3
|
+
riko.bado
|
|
4
|
+
~~~~~~~~~
|
|
5
|
+
Provides functions for creating asynchronous riko pipes
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
basic usage::
|
|
9
|
+
|
|
10
|
+
>>> from riko import get_path
|
|
11
|
+
>>> from riko.bado import react
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from twisted.internet.task import react
|
|
17
|
+
except ImportError:
|
|
18
|
+
react = lambda _, _reactor=None: None
|
|
19
|
+
inlineCallbacks = lambda _: lambda: None
|
|
20
|
+
returnValue = lambda _: lambda: None
|
|
21
|
+
backend = "empty"
|
|
22
|
+
else:
|
|
23
|
+
from twisted.internet.defer import inlineCallbacks, returnValue
|
|
24
|
+
|
|
25
|
+
backend = "twisted"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Reactor:
|
|
29
|
+
fake = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
reactor = Reactor()
|
|
33
|
+
coroutine = inlineCallbacks
|
|
34
|
+
return_value = returnValue
|
|
35
|
+
_issync = backend == "empty"
|
|
36
|
+
_isasync = not _issync
|
riko/bado/io.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# vim: sw=4:ts=4:expandtab
|
|
2
|
+
"""
|
|
3
|
+
riko.bado.io
|
|
4
|
+
~~~~~~~~~~~~
|
|
5
|
+
Provides functions for asynchronously reading files and urls
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
basic usage::
|
|
9
|
+
|
|
10
|
+
>>> from riko import get_path
|
|
11
|
+
>>> from riko.bado.io import async_url_open
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import builtins
|
|
16
|
+
from collections.abc import Generator
|
|
17
|
+
from io import StringIO
|
|
18
|
+
from os import remove
|
|
19
|
+
from tempfile import NamedTemporaryFile
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
import pygogo as gogo
|
|
23
|
+
|
|
24
|
+
from riko import ENCODING, get_abspath
|
|
25
|
+
|
|
26
|
+
from . import coroutine, return_value
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from twisted.internet.testing import AccumulatingProtocol
|
|
30
|
+
except ImportError:
|
|
31
|
+
AccumulatingProtocol = callLater = StringTransport = FileSender = treq = object
|
|
32
|
+
else:
|
|
33
|
+
import treq
|
|
34
|
+
from twisted.internet.defer import Deferred
|
|
35
|
+
from twisted.internet.reactor import callLater
|
|
36
|
+
from twisted.internet.testing import StringTransport
|
|
37
|
+
from twisted.protocols.basic import FileSender
|
|
38
|
+
|
|
39
|
+
logger = gogo.Gogo(__name__, monolog=True).logger
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# http://stackoverflow.com/q/26314586/408556
|
|
43
|
+
# http://stackoverflow.com/q/8157197/408556
|
|
44
|
+
# http://stackoverflow.com/a/33708936/408556
|
|
45
|
+
class FileReader(AccumulatingProtocol):
|
|
46
|
+
def __init__(self, filename, transform=None, delay=0, verbose=False):
|
|
47
|
+
self.f = builtins.open(filename, "rb")
|
|
48
|
+
self.transform = transform
|
|
49
|
+
self.delay = delay
|
|
50
|
+
self.producer = FileSender()
|
|
51
|
+
self.logger = gogo.Gogo(__name__, verbose=verbose).logger
|
|
52
|
+
|
|
53
|
+
def cleanup(self, *args):
|
|
54
|
+
self.f.close()
|
|
55
|
+
self.producer.stopProducing()
|
|
56
|
+
|
|
57
|
+
def resumeProducing(self):
|
|
58
|
+
chunk = self.file.read(self.CHUNK_SIZE) if self.file else ""
|
|
59
|
+
|
|
60
|
+
if not chunk:
|
|
61
|
+
self.file = None
|
|
62
|
+
self.consumer.unregisterProducer()
|
|
63
|
+
|
|
64
|
+
if self.deferred and self.delay:
|
|
65
|
+
callLater(self.delay, self.deferred.callback, self.lastSent) # pyright: ignore[reportCallIssue]
|
|
66
|
+
elif self.deferred:
|
|
67
|
+
self.deferred.callback(self.lastSent)
|
|
68
|
+
|
|
69
|
+
self.deferred = None
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
def connectionLost(self, reason):
|
|
73
|
+
self.logger.debug(f"connectionLost: {reason}")
|
|
74
|
+
self.cleanup()
|
|
75
|
+
|
|
76
|
+
def connectionMade(self):
|
|
77
|
+
self.logger.debug(f"Connection made from {self.transport.getPeer()}")
|
|
78
|
+
args = (self.f, self.transport, self.transform)
|
|
79
|
+
self.d = self.closedDeferred = self.producer.beginFileTransfer(*args)
|
|
80
|
+
|
|
81
|
+
while not self.d.called:
|
|
82
|
+
self.producer.resumeProducing()
|
|
83
|
+
|
|
84
|
+
self.d.addErrback(self.logger.error)
|
|
85
|
+
self.d.addBoth(self.cleanup)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@coroutine # pyright: ignore[reportArgumentType]
|
|
89
|
+
def async_read_file(
|
|
90
|
+
filename: str, transport, protocol=FileReader, encoding=ENCODING, **kwargs
|
|
91
|
+
) -> Generator[Deferred[str], str, None]:
|
|
92
|
+
proto = protocol(filename.replace("file://", ""), **kwargs)
|
|
93
|
+
proto.makeConnection(transport)
|
|
94
|
+
yield proto.d
|
|
95
|
+
value: bytes = proto.transport.value()
|
|
96
|
+
return_value(value.decode(encoding))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@coroutine # pyright: ignore[reportArgumentType]
|
|
100
|
+
def async_get_file(
|
|
101
|
+
filename: str, transport, protocol=FileReader, **kwargs
|
|
102
|
+
) -> Generator[Deferred[StringIO], StringIO, None]:
|
|
103
|
+
proto = protocol(filename.replace("file://", ""), **kwargs)
|
|
104
|
+
proto.makeConnection(transport)
|
|
105
|
+
yield proto.d
|
|
106
|
+
proto.transport.io.seek(0)
|
|
107
|
+
f = cast(StringIO, proto.transport.io)
|
|
108
|
+
return_value(f)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@coroutine # pyright: ignore[reportArgumentType]
|
|
112
|
+
def async_url_open(
|
|
113
|
+
url: str, timeout=0, encoding=ENCODING, **kwargs
|
|
114
|
+
) -> Generator[Deferred[StringIO], StringIO, None]:
|
|
115
|
+
if url.startswith("http"):
|
|
116
|
+
page = NamedTemporaryFile(delete=False, mode="w")
|
|
117
|
+
new_url = page.name
|
|
118
|
+
response = yield treq.get(url) # pyright: ignore[reportAttributeAccessIssue]
|
|
119
|
+
content = yield response.text() # pyright: ignore[reportAttributeAccessIssue]
|
|
120
|
+
page.write(cast(str, content))
|
|
121
|
+
page.flush()
|
|
122
|
+
file_name = url.split("://")[1] if url.startswith("file") else None
|
|
123
|
+
else:
|
|
124
|
+
page, new_url, file_name = None, url, None
|
|
125
|
+
|
|
126
|
+
f = yield async_get_file(new_url, StringTransport(), **kwargs) # pyright: ignore[reportCallIssue]
|
|
127
|
+
|
|
128
|
+
if not hasattr(f, "name") and file_name:
|
|
129
|
+
f.name = file_name
|
|
130
|
+
|
|
131
|
+
if page:
|
|
132
|
+
page.close()
|
|
133
|
+
remove(page.name)
|
|
134
|
+
|
|
135
|
+
return_value(f)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@coroutine # pyright: ignore[reportArgumentType]
|
|
139
|
+
def async_url_read(
|
|
140
|
+
url: str, timeout=0, **kwargs
|
|
141
|
+
) -> Generator[Deferred[str], str, None]:
|
|
142
|
+
url = get_abspath(url, offline=True)
|
|
143
|
+
|
|
144
|
+
if url.startswith("http"):
|
|
145
|
+
response = yield treq.get(url) # pyright: ignore[reportAttributeAccessIssue]
|
|
146
|
+
content = yield response.text() # pyright: ignore[reportAttributeAccessIssue]
|
|
147
|
+
else:
|
|
148
|
+
content = yield async_read_file(url, StringTransport(), **kwargs) # pyright: ignore[reportReturnType, reportCallIssue]
|
|
149
|
+
|
|
150
|
+
return_value(content)
|
riko/bado/itertools.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# vim: sw=4:ts=4:expandtab
|
|
2
|
+
"""
|
|
3
|
+
riko.bado.itertools
|
|
4
|
+
~~~~~~~~~~~~~~~~~~~
|
|
5
|
+
Provides asynchronous ports of various builtin itertools functions
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
basic usage::
|
|
9
|
+
|
|
10
|
+
>>> from riko import get_path
|
|
11
|
+
>>> from riko.bado.itertools import coop_reduce
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import itertools as it
|
|
16
|
+
from functools import partial
|
|
17
|
+
|
|
18
|
+
from . import coroutine, reactor, return_value
|
|
19
|
+
from .mock import FakeReactor
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from twisted.internet.task import Cooperator
|
|
23
|
+
except ImportError:
|
|
24
|
+
Cooperator = real_task = gatherResults = None
|
|
25
|
+
else:
|
|
26
|
+
from twisted.internet import task as real_task
|
|
27
|
+
from twisted.internet.defer import gatherResults
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_task():
|
|
31
|
+
if reactor.fake:
|
|
32
|
+
task = Cooperator(
|
|
33
|
+
scheduler=partial(FakeReactor().callLater, FakeReactor._DELAY)
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
task = real_task.Cooperator()
|
|
37
|
+
|
|
38
|
+
return task
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@coroutine
|
|
42
|
+
def coop_reduce(func, iterable, initializer=None):
|
|
43
|
+
task = get_task()
|
|
44
|
+
iterable = iter(iterable)
|
|
45
|
+
x = initializer or next(iterable)
|
|
46
|
+
result = {}
|
|
47
|
+
|
|
48
|
+
def work(func, it, x):
|
|
49
|
+
for y in it:
|
|
50
|
+
result["value"] = x = func(x, y)
|
|
51
|
+
yield
|
|
52
|
+
|
|
53
|
+
_task = task.cooperate(work(func, iterable, x))
|
|
54
|
+
yield _task.whenDone()
|
|
55
|
+
return_value(result["value"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def async_reduce(async_func, iterable, initializer=None):
|
|
59
|
+
it = iter(iterable)
|
|
60
|
+
x = initializer or next(it)
|
|
61
|
+
|
|
62
|
+
@coroutine
|
|
63
|
+
def work(async_func, it, x):
|
|
64
|
+
for y in it:
|
|
65
|
+
x = yield async_func(x, y)
|
|
66
|
+
|
|
67
|
+
return_value(x)
|
|
68
|
+
|
|
69
|
+
return work(async_func, it, x)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@coroutine
|
|
73
|
+
def async_map(afunc, iterable, connections=0, **kwargs):
|
|
74
|
+
"""
|
|
75
|
+
Parallel map for deferred callables using cooperative multitasking
|
|
76
|
+
http://stackoverflow.com/a/20376166/408556
|
|
77
|
+
"""
|
|
78
|
+
if connections and not reactor.fake:
|
|
79
|
+
results = []
|
|
80
|
+
work = (afunc(x, **kwargs).addCallback(results.append) for x in iterable)
|
|
81
|
+
deferreds = [get_task().coiterate(work) for _ in range(connections)]
|
|
82
|
+
yield gatherResults(deferreds, consumeErrors=True)
|
|
83
|
+
else:
|
|
84
|
+
afunc = partial(afunc, **kwargs)
|
|
85
|
+
deferreds = map(afunc, iterable)
|
|
86
|
+
results = yield gatherResults(deferreds, consumeErrors=True)
|
|
87
|
+
|
|
88
|
+
return_value(results)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def async_starmap(async_func, iterable):
|
|
92
|
+
"""itertools.starmap for deferred callables"""
|
|
93
|
+
deferreds = it.starmap(async_func, iterable)
|
|
94
|
+
return gatherResults(deferreds, consumeErrors=True)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def async_dispatch(split, *async_funcs, **kwargs):
|
|
98
|
+
return async_starmap(lambda item, f: f(item), zip(split, async_funcs, strict=False))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def async_broadcast(item, *async_funcs, **kwargs):
|
|
102
|
+
return async_dispatch(it.repeat(item), *async_funcs, **kwargs)
|