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.
Files changed (170) hide show
  1. riko/.DS_Store +0 -0
  2. riko/__init__.py +321 -0
  3. riko/_pubsub/.DS_Store +0 -0
  4. riko/autorss.py +89 -0
  5. riko/bado/.DS_Store +0 -0
  6. riko/bado/__init__.py +36 -0
  7. riko/bado/io.py +150 -0
  8. riko/bado/itertools.py +102 -0
  9. riko/bado/microdom.py +1113 -0
  10. riko/bado/mock.py +74 -0
  11. riko/bado/requests.py +22 -0
  12. riko/bado/sux.py +680 -0
  13. riko/bado/util.py +93 -0
  14. riko/cast.py +386 -0
  15. riko/cli/.DS_Store +0 -0
  16. riko/cli/__init__.py +0 -0
  17. riko/cli/benchmark.py +173 -0
  18. riko/cli/manage.py +255 -0
  19. riko/cli/runpipe.py +79 -0
  20. riko/collections.py +642 -0
  21. riko/compile.py +358 -0
  22. riko/currencies.py +1208 -0
  23. riko/data/.DS_Store +0 -0
  24. riko/data/Pinterest.html +3617 -0
  25. riko/data/Politik.xml +260 -0
  26. riko/data/TheEdTechie.xml +423 -0
  27. riko/data/Topthemen.xml +249 -0
  28. riko/data/api.github.com_search_users?q=defunkt.json +622 -0
  29. riko/data/autoblog.xml +1312 -0
  30. riko/data/bbc.html +1301 -0
  31. riko/data/bbci.co.uk.xml +323 -0
  32. riko/data/blog.ouseful.info_feed.xml +502 -0
  33. riko/data/caltrain.html +566 -0
  34. riko/data/capnorth.oes.ca.gov_89509.xml +73 -0
  35. riko/data/capnorth.xml +73 -0
  36. riko/data/cnn.html +1771 -0
  37. riko/data/countries.csv +260 -0
  38. riko/data/currencies.csv +167 -0
  39. riko/data/currencies.json +1064 -0
  40. riko/data/delicious.xml +328 -0
  41. riko/data/edition.cnn.html +1100 -0
  42. riko/data/elance.json +1730 -0
  43. riko/data/election_results.json +25822 -0
  44. riko/data/feed.xml +217 -0
  45. riko/data/feeds.delicious.com_v2_rss_popular?count=15.xml +328 -0
  46. riko/data/feeds.delicious.com_v2_rss_popular?count=3.xml +84 -0
  47. riko/data/feeds.delicious.com_v2_rss_psychemedia?count=15.xml +267 -0
  48. riko/data/feeds.feedburner.com_TheEdTechie.xml +313 -0
  49. riko/data/feeds.feedburner.com_ouseful.xml +546 -0
  50. riko/data/feeds.feedburner.com_yodelanecdotal.xml +97 -0
  51. riko/data/feeds.gawker.com_jalopnik_full.xml +2 -0
  52. riko/data/fourtitude.xml +120 -0
  53. riko/data/freelancer.json +465 -0
  54. riko/data/gawker.xml +308 -0
  55. riko/data/gigs.json +1336 -0
  56. riko/data/greenhughes.xml +109 -0
  57. riko/data/guru.json +1729 -0
  58. riko/data/health.xml +413 -0
  59. riko/data/kazeeki.json +4135 -0
  60. riko/data/kazeeki2.json +119 -0
  61. riko/data/kazeeki3.json +27 -0
  62. riko/data/lorem.txt +32 -0
  63. riko/data/news.google.com_rss?hl=en-US&gl=US&ceid=US_3Aen&q=enterprise+mashup.xml +524 -0
  64. riko/data/news.yahoo.com_rss_health.xml +414 -0
  65. riko/data/news.yahoo.com_rss_topstories.html +1343 -0
  66. riko/data/news.yahoo.com_rss_topstories.xml +443 -0
  67. riko/data/odesk.json +224 -0
  68. riko/data/ouseful.xml +580 -0
  69. riko/data/ouseful_feedburner.xml +673 -0
  70. riko/data/places.xml +31 -0
  71. riko/data/podcast.xml +661 -0
  72. riko/data/psychemedia_delicious.xml +261 -0
  73. riko/data/psychemedia_slideshare.xml +891 -0
  74. riko/data/quote.json +178 -0
  75. riko/data/rsc-ne-scotland.org.uk_mashe_ititle_xml_jisc10bean.xml +390 -0
  76. riko/data/rss.sueddeutsche.de_rss_Politik.xml +215 -0
  77. riko/data/rss.sueddeutsche.de_rss_Topthemen.xml +204 -0
  78. riko/data/schools.xml +20 -0
  79. riko/data/sciencedaily.html +873 -0
  80. riko/data/scotland.xml +391 -0
  81. riko/data/spreadsheet.csv +646 -0
  82. riko/data/spreadsheets.google.com_pub?key=p1rHUqg4g420UMaN1sPvaRg&output=csv&range=a1_AB646.csv +646 -0
  83. riko/data/status.csv +1 -0
  84. riko/data/topstories.xml +443 -0
  85. riko/data/users.jyu.fi.html +45 -0
  86. riko/data/users.jyu.fi_~atsoukka_cgi_bin_aarresaari.html +37 -0
  87. riko/data/www.autoblog.com_rss.xml +435 -0
  88. riko/data/www.bbc.co.uk_news.html +3251 -0
  89. riko/data/www.bbc.co.uk_programmes_b006mvlc_episodes_player.xml +661 -0
  90. riko/data/www.caltrain.com_Fares_farechart.html +527 -0
  91. riko/data/www.fourtitude.com_news_publish_rss.xml +313 -0
  92. riko/data/www.greenhughes.com_rssfeed.xml +238 -0
  93. riko/data/www.sciencedaily.com_rss_computers_math.html +811 -0
  94. riko/data/www.slideshare.net_rss_user_psychemedia.xml +891 -0
  95. riko/data/www.theguardian.com_politics_api_general_election_2010_results_json.json +1 -0
  96. riko/data/yodel.xml +447 -0
  97. riko/data/yql.xml +21 -0
  98. riko/dates.py +145 -0
  99. riko/dotdict.py +423 -0
  100. riko/helpers.py +105 -0
  101. riko/locations.py +1769 -0
  102. riko/modules/.DS_Store +0 -0
  103. riko/modules/__init__.py +971 -0
  104. riko/modules/aggregate.py +120 -0
  105. riko/modules/count.py +173 -0
  106. riko/modules/csv.py +239 -0
  107. riko/modules/currencyformat.py +147 -0
  108. riko/modules/datebuilder.py +148 -0
  109. riko/modules/dateformat.py +156 -0
  110. riko/modules/exchangerate.py +318 -0
  111. riko/modules/feedautodiscovery.py +212 -0
  112. riko/modules/fetch.py +214 -0
  113. riko/modules/fetchdata.py +223 -0
  114. riko/modules/fetchpage.py +235 -0
  115. riko/modules/fetchsitefeed.py +203 -0
  116. riko/modules/fetchtable.py +239 -0
  117. riko/modules/fetchtext.py +204 -0
  118. riko/modules/filter.py +290 -0
  119. riko/modules/forever.py +135 -0
  120. riko/modules/geolocate.py +155 -0
  121. riko/modules/hash.py +136 -0
  122. riko/modules/input.py +226 -0
  123. riko/modules/itembuilder.py +150 -0
  124. riko/modules/join.py +195 -0
  125. riko/modules/loop.py +103 -0
  126. riko/modules/numberinput.py +20 -0
  127. riko/modules/receive.py +176 -0
  128. riko/modules/refind.py +257 -0
  129. riko/modules/regex.py +311 -0
  130. riko/modules/rename.py +231 -0
  131. riko/modules/reverse.py +115 -0
  132. riko/modules/rssitembuilder.py +194 -0
  133. riko/modules/send.py +124 -0
  134. riko/modules/simplemath.py +168 -0
  135. riko/modules/slugify.py +129 -0
  136. riko/modules/sort.py +224 -0
  137. riko/modules/split.py +143 -0
  138. riko/modules/strconcat.py +151 -0
  139. riko/modules/strfind.py +250 -0
  140. riko/modules/strreplace.py +222 -0
  141. riko/modules/strtransform.py +240 -0
  142. riko/modules/subelement.py +200 -0
  143. riko/modules/substr.py +149 -0
  144. riko/modules/sum.py +185 -0
  145. riko/modules/tail.py +131 -0
  146. riko/modules/timeout.py +219 -0
  147. riko/modules/tokenizer.py +167 -0
  148. riko/modules/truncate.py +141 -0
  149. riko/modules/typecast.py +154 -0
  150. riko/modules/udf.py +119 -0
  151. riko/modules/union.py +139 -0
  152. riko/modules/uniq.py +158 -0
  153. riko/modules/urlbuilder.py +212 -0
  154. riko/modules/urlinput.py +21 -0
  155. riko/modules/urlparse.py +137 -0
  156. riko/modules/xpathfetchpage.py +287 -0
  157. riko/parsers.py +540 -0
  158. riko/pprint2.py +131 -0
  159. riko/templates/pypipe.txt +35 -0
  160. riko/topsort.py +163 -0
  161. riko/types/.DS_Store +0 -0
  162. riko/types/compile.py +113 -0
  163. riko/types/general.py +354 -0
  164. riko/types/modules.py +106 -0
  165. riko/utils.py +897 -0
  166. riko-0.68.1.dist-info/METADATA +756 -0
  167. riko-0.68.1.dist-info/RECORD +170 -0
  168. riko-0.68.1.dist-info/WHEEL +4 -0
  169. riko-0.68.1.dist-info/entry_points.txt +5 -0
  170. 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)