wukong 0.1.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/INSTALL.textile +89 -0
  2. data/README.textile +41 -74
  3. data/docpages/INSTALL.textile +94 -0
  4. data/{doc → docpages}/LICENSE.textile +0 -0
  5. data/{doc → docpages}/README-wulign.textile +6 -0
  6. data/docpages/UsingWukong-part1-get_ready.textile +17 -0
  7. data/{doc/overview.textile → docpages/UsingWukong-part2-ThinkingBigData.textile} +8 -24
  8. data/{doc → docpages}/UsingWukong-part3-parsing.textile +8 -2
  9. data/docpages/_config.yml +39 -0
  10. data/{doc/tips.textile → docpages/bigdata-tips.textile} +71 -44
  11. data/{doc → docpages}/code/api_response_example.txt +0 -0
  12. data/{doc → docpages}/code/parser_skeleton.rb +0 -0
  13. data/{doc/intro_to_map_reduce → docpages/diagrams}/MapReduceDiagram.graffle +0 -0
  14. data/docpages/favicon.ico +0 -0
  15. data/docpages/gem.css +16 -0
  16. data/docpages/hadoop-tips.textile +83 -0
  17. data/docpages/index.textile +90 -0
  18. data/docpages/intro.textile +8 -0
  19. data/docpages/moreinfo.textile +174 -0
  20. data/docpages/news.html +24 -0
  21. data/{doc → docpages}/pig/PigLatinExpressionsList.txt +0 -0
  22. data/{doc → docpages}/pig/PigLatinReferenceManual.html +0 -0
  23. data/{doc → docpages}/pig/PigLatinReferenceManual.txt +0 -0
  24. data/docpages/tutorial.textile +283 -0
  25. data/docpages/usage.textile +195 -0
  26. data/docpages/wutils.textile +263 -0
  27. data/wukong.gemspec +80 -50
  28. metadata +87 -54
  29. data/doc/INSTALL.textile +0 -41
  30. data/doc/README-tutorial.textile +0 -163
  31. data/doc/README-wutils.textile +0 -128
  32. data/doc/TODO.textile +0 -61
  33. data/doc/UsingWukong-part1-setup.textile +0 -2
  34. data/doc/UsingWukong-part2-scraping.textile +0 -2
  35. data/doc/hadoop-nfs.textile +0 -51
  36. data/doc/hadoop-setup.textile +0 -29
  37. data/doc/index.textile +0 -124
  38. data/doc/links.textile +0 -42
  39. data/doc/usage.textile +0 -102
  40. data/doc/utils.textile +0 -48
  41. data/examples/and_pig/sample_queries.rb +0 -128
  42. data/lib/wukong/and_pig.rb +0 -62
  43. data/lib/wukong/and_pig/README.textile +0 -12
  44. data/lib/wukong/and_pig/as.rb +0 -37
  45. data/lib/wukong/and_pig/data_types.rb +0 -30
  46. data/lib/wukong/and_pig/functions.rb +0 -50
  47. data/lib/wukong/and_pig/generate.rb +0 -85
  48. data/lib/wukong/and_pig/generate/variable_inflections.rb +0 -82
  49. data/lib/wukong/and_pig/junk.rb +0 -51
  50. data/lib/wukong/and_pig/operators.rb +0 -8
  51. data/lib/wukong/and_pig/operators/compound.rb +0 -29
  52. data/lib/wukong/and_pig/operators/evaluators.rb +0 -7
  53. data/lib/wukong/and_pig/operators/execution.rb +0 -15
  54. data/lib/wukong/and_pig/operators/file_methods.rb +0 -29
  55. data/lib/wukong/and_pig/operators/foreach.rb +0 -98
  56. data/lib/wukong/and_pig/operators/groupies.rb +0 -212
  57. data/lib/wukong/and_pig/operators/load_store.rb +0 -65
  58. data/lib/wukong/and_pig/operators/meta.rb +0 -42
  59. data/lib/wukong/and_pig/operators/relational.rb +0 -129
  60. data/lib/wukong/and_pig/pig_struct.rb +0 -48
  61. data/lib/wukong/and_pig/pig_var.rb +0 -95
  62. data/lib/wukong/and_pig/symbol.rb +0 -29
  63. data/lib/wukong/and_pig/utils.rb +0 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ layout: default
3
+ title: edamame news
4
+ collapse: true
5
+ ---
6
+ <h1 class="gemheader">{% if site.gemname %}{{ site.gemname }}{% else %}mrflip{% endif %}<span class="small">:: news</span></h1>
7
+
8
+ <div id="news">
9
+ {% for t in site.posts %} {% assign has_posts = true %}{% endfor %}{% if has_posts %}
10
+ {% for post in site.posts %}
11
+ <div class="toggle" id="news-{{ post.id }}">
12
+
13
+ <h2><a href="{{ post.url }}">{{ post.title }}</a><span class="postdate"> &raquo; {{ post.date | date_to_string }}</span></h2>
14
+
15
+ {{ post.content }}
16
+
17
+ </div>
18
+ {% endfor %}
19
+ {% else %}
20
+ <p class="heavy">
21
+ <em>(no news. good news?)</em>
22
+ </p>
23
+ {% endif %}
24
+ </div>
File without changes
File without changes
File without changes
@@ -0,0 +1,283 @@
1
+ ---
2
+ layout: default
3
+ title: mrflip.github.com/wukong - Tutorial
4
+ collapse: false
5
+ ---
6
+
7
+ h1(gemheader). Tutorial by Examples
8
+
9
+
10
+ <notextile><div class="toggle"></notextile>
11
+
12
+ h2(#wordcount). Count Words
13
+
14
+ Here's a script to count words in a text stream:
15
+
16
+ {% highlight ruby %}
17
+ require 'wukong'
18
+ module WordCount
19
+ class Mapper < Wukong::Streamer::LineStreamer
20
+ # Emit each word in the line.
21
+ def process line
22
+ words = line.strip.split(/\W+/).reject(&:blank?)
23
+ words.each{|word| yield [word, 1] }
24
+ end
25
+ end
26
+
27
+ class Reducer < Wukong::Streamer::ListReducer
28
+ def finalize
29
+ yield [ key, values.map(&:last).map(&:to_i).sum ]
30
+ end
31
+ end
32
+ end
33
+
34
+ Wukong::Script.new(
35
+ WordCount::Mapper,
36
+ WordCount::Reducer
37
+ ).run # Execute the script
38
+ {% endhighlight %}
39
+
40
+ The first class, the Mapper, eats lines and craps @[word, count]@ records. Here
41
+ the /key/ is the word, and the /value/ is its count.
42
+
43
+ The second class is an example of an accumulated list reducer. The values for
44
+ each key are stacked up into a list; then the record(s) yielded by @#finalize@
45
+ are emitted.
46
+
47
+ Here's another way to write the Reducer: accumulate the count of each line, then
48
+ yield the sum in @#finalize@:
49
+
50
+ {% highlight ruby %}
51
+ class Reducer2 < Wukong::Streamer::AccumulatingReducer
52
+ attr_accessor :key_count
53
+ def start! *args
54
+ self.key_count = 0
55
+ end
56
+ def accumulate(word, count)
57
+ self.key_count += count.to_i
58
+ end
59
+ def finalize
60
+ yield [ key, key_count ]
61
+ end
62
+ end
63
+ {% endhighlight %}
64
+
65
+ Of course you can be really lazy (i.e. smart) and write your script as
66
+
67
+ {% highlight ruby %}
68
+ class Script < Wukong::Script
69
+ def reducer_command
70
+ 'uniq -c'
71
+ end
72
+ end
73
+ {% endhighlight %}
74
+
75
+ h2(#structstream). Structured data
76
+
77
+ The previous example dealt with unstructured data. Wukong also lets you view your data as a stream of structured objects.
78
+
79
+ Let's say you have a blog; its records look like
80
+
81
+ {% highlight ruby %}
82
+ Post = Struct.new( :id, :created_at, :user_id, :title, :body, :link )
83
+ Comment = Struct.new( :id, :created_at, :post_id, :user_id, :body )
84
+ User = Struct.new( :id, :username, :fullname, :homepage, :description )
85
+ UserLoc = Struct.new( :user_id, :text, :lat, :lng )
86
+ {% endhighlight %}
87
+
88
+ You've been using "twitter":http://twitter.com for a long time, and you've written something that from now on will inject all your tweets as Posts, and all replies to them as Comments (by a common 'twitter_bot' account on your blog).What about the past two years' worth of tweets? Let's assume you're so chatty that a Map/Reduce script is warranted to handle the volume. (Actually, wukong makes a really nice ETL package, so this may be convienient even at small scale).
89
+
90
+ Cook up something that scrapes your tweets and all replies to your tweets:
91
+
92
+ {% highlight ruby %}
93
+ Tweet = Struct.new( :id, :created_at, :twitter_user_id,
94
+ :in_reply_to_user_id, :in_reply_to_status_id, :text )
95
+ TwitterUser = Struct.new( :id, :username, :fullname,
96
+ :homepage, :location, :description )
97
+ {% endhighlight %}
98
+
99
+ Now we'll just process all those in a big pile, converting to Posts, Comments and Users as appropriate. Serialize your scrape results so that each Tweet and each TwitterUser is a single lines containing first the class name ('tweet' or 'twitter_user') followed by its constituent fields, in order, separated by tabs.
100
+
101
+ The RecordStreamer takes each such line, constructs its corresponding class, and instantiates it with the
102
+
103
+ {% highlight ruby %}
104
+ require 'wukong'
105
+ require 'my_blog' #defines the blog models
106
+ module TwitBlog
107
+ class Mapper < Wukong::Streamer::RecordStreamer
108
+ # Watch for tweets by me
109
+ MY_USER_ID = 24601
110
+ # structs for our input objects
111
+ Tweet = Struct.new( :id, :created_at, :twitter_user_id,
112
+ :in_reply_to_user_id, :in_reply_to_status_id, :text )
113
+ TwitterUser = Struct.new( :id, :username, :fullname,
114
+ :homepage, :location, :description )
115
+ #
116
+ # If this is a tweet is by me, convert it to a Post.
117
+ #
118
+ # If it is a tweet not by me, convert it to a Comment that
119
+ # will be paired with the correct Post.
120
+ #
121
+ # If it is a TwitterUser, convert it to a User record and
122
+ # a user_location record
123
+ #
124
+ def process record
125
+ case record
126
+ when TwitterUser
127
+ user = MyBlog::User.new.merge(record) # grab the fields in common
128
+ user_loc = MyBlog::UserLoc.new(record.id, record.location, nil, nil)
129
+ yield user
130
+ yield user_loc
131
+ when Tweet
132
+ if record.twitter_user_id == MY_USER_ID
133
+ post = MyBlog::Post.new.merge record
134
+ post.link = "http://twitter.com/statuses/show/#{record.id}"
135
+ post.body = record.text
136
+ post.title = record.text[0..65] + "..."
137
+ yield post
138
+ else
139
+ comment = MyBlog::Comment.new.merge record
140
+ comment.body = record.text
141
+ comment.post_id = record.in_reply_to_status_id
142
+ yield comment
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ Wukong::Script.new( TwitBlog::Mapper, nil ).run # identity reducer
149
+ {% endhighlight %}
150
+
151
+ h2(#accumulators). Accumulators
152
+
153
+ h3(#uniqifying). A Uniqifying Accumulator
154
+
155
+
156
+ The script above uses the identity reducer: every record from the mapper is sent
157
+ to the output. But what if you had grabbed the replying user's record every time you saw a reply?
158
+
159
+ You'd like to just pass it through @uniq@. But if something has changed in the interim, or if you record a timestamp for each sample, you won't be able to use the simple @uniq@ command. You'd like to just get one example for each key!
160
+
161
+ Wukong includes just such a reducer, the UniqByLastReducer:
162
+
163
+ {% highlight ruby %}
164
+ #
165
+ # UniqByLastReducer accepts all records for a given key and emits only the
166
+ # last-seen.
167
+ #
168
+ # It acts like an insecure high-school kid: for each record of a given key
169
+ # it discards whatever record it's holding and adopts this new value. When a
170
+ # new key comes on the scene it emits the last record, like an older brother
171
+ # handing off his Depeche Mode collection.
172
+ #
173
+ # For example, to extract the *latest* value for each property, emit your
174
+ # records as
175
+ #
176
+ # [resource_type, key, timestamp, ... fields ...]
177
+ #
178
+ # then set :sort_fields to 3 and :partition_fields to 2.
179
+ #
180
+ class UniqByLastReducer < Wukong::Streamer::AccumulatingReducer
181
+ attr_accessor :final_value
182
+
183
+ #
184
+ # Use first two fields as keys by default
185
+ #
186
+ def get_key *vals
187
+ vals[0..1]
188
+ end
189
+
190
+ #
191
+ # Adopt each value in turn: the last one's the one you want.
192
+ #
193
+ def accumulate *vals
194
+ self.final_value = vals
195
+ end
196
+
197
+ #
198
+ # Emit the last-seen value
199
+ #
200
+ def finalize
201
+ yield final_value if final_value
202
+ end
203
+
204
+ #
205
+ # Clear state on reset
206
+ #
207
+ def start! *args
208
+ self.final_value = nil
209
+ end
210
+ end
211
+ {% endhighlight %}
212
+
213
+ h3(#groupby). A GroupBy Accumulator
214
+
215
+ Wukong has a good collection of map/reduce patterns. For example, it's quite common to accumulate all records for a given key and emit some result based on the whole group. The
216
+
217
+ The AccumulatingReducer calls start! on the first record for each key, calls accumulate() on every example for that key (including the first), and calls finalize() once the last record for that key is seen.
218
+
219
+ Here's an AccumulatingReducer that takes a long list of key-value pairs and emits, for each key, all its corresponding values in one line.
220
+
221
+ {% highlight ruby %}
222
+ #
223
+ # Roll up all values for each key into a single line
224
+ #
225
+ class GroupByReducer < Wukong::Streamer::AccumulatingReducer
226
+ attr_accessor :values
227
+
228
+ # Start with an empty list
229
+ def start! *args
230
+ self.values = []
231
+ end
232
+
233
+ # Aggregate each value in turn
234
+ def accumulate key, value
235
+ self.values << value
236
+ end
237
+
238
+ # Emit the key and all values, tab-separated
239
+ def finalize
240
+ yield [key, values].flatten
241
+ end
242
+ end
243
+ {% endhighlight %}
244
+
245
+ So given adjacency pairs for the following directed friend graph:
246
+
247
+ <pre>
248
+ @jerry @elaine
249
+ @elaine @jerry
250
+ @jerry @kramer
251
+ @kramer @jerry
252
+ @kramer @bobsacamato
253
+ @kramer @newman
254
+ @jerry @superman
255
+ @newman @kramer
256
+ @newman @elaine
257
+ @newman @jerry
258
+ </pre>
259
+
260
+ You'd end up with
261
+
262
+ <pre> @elaine @jerry
263
+ @jerry @elaine @kramer @superman
264
+ @kramer @bobsacamato @jerry @newman
265
+ @newman @elaine @jerry @kramer
266
+ </pre>
267
+
268
+
269
+ h2. A note about keys
270
+
271
+ Now we're going to write this using the synthetic keys already extant in the
272
+ twitter records, making the unwarranted assumption that they won't collide with
273
+ the keys in your database.
274
+
275
+ Map/Reduce paradigm does badly with synthetic keys. Synthetic keys demand
276
+ locality, and map/reduce's remarkable scaling comes from not assuming
277
+ locality. In general, write your map/reduce scripts to use natural keys (the scre
278
+
279
+ h2. More...
280
+
281
+ There are many useful examples (including an actually-useful version of this
282
+ WordCount script) in the "examples/ directory.":http://github.com/mrflip/wukong/tree/master/examples
283
+
@@ -0,0 +1,195 @@
1
+ ---
2
+ layout: default
3
+ title: Usage notes
4
+ ---
5
+
6
+ h1(gemheader). {{ site.gemname }} %(small):: usage%
7
+
8
+ ** "How to run a Wukong script":#running
9
+ ** "How to test your scripts":#testing
10
+ ** "Wukong Plays nicely with others":#playnice
11
+ ** "Schema export":#schema_export to Pig or SQL
12
+ ** "Wukong's internal workflow":#workflow
13
+ ** "Using wukong with internal streaming":#stayinruby
14
+ ** "Using wukong to Batch-Process ActiveRecord Objects":#activerecord
15
+
16
+
17
+ <notextile><div class="toggle"></notextile>
18
+
19
+ h2(#running). How to run a Wukong script
20
+
21
+ To run your script using local files and no connection to a hadoop cluster,
22
+
23
+ pre. your/script.rb --run=local path/to/input_files path/to/output_dir
24
+
25
+ To run the command across a Hadoop cluster,
26
+
27
+ pre. your/script.rb --run=hadoop path/to/input_files path/to/output_dir
28
+
29
+ You can set the default in the config/wukong-site.yaml file, and then just use @--run@ instead of @--run=something@ --it will just use the default run mode.
30
+
31
+ If you're running @--run=hadoop@, all file paths are HDFS paths. If you're running @--run=local@, all file paths are local paths. (your/script path, of course, lives on the local filesystem).
32
+
33
+ You can supply arbitrary command line arguments (they wind up as key-value pairs in the options path your mapper and reducer receive), and you can use the hadoop syntax to specify more than one input file:
34
+
35
+ pre. ./path/to/your/script.rb --any_specific_options --options=can_have_vals \
36
+ --run "input_dir/part_*,input_file2.tsv,etc.tsv" path/to/output_dir
37
+
38
+ Note that all @--options@ must precede (in any order) all non-options.
39
+
40
+ <notextile></div><div class="toggle"></notextile>
41
+
42
+ h2(#testing). How to test your scripts
43
+
44
+ To run mapper on its own:
45
+
46
+ pre. cat ./local/test/input.tsv | ./examples/word_count.rb --map | more
47
+
48
+ or if your test data lies on the HDFS,
49
+
50
+ pre. hdp-cat test/input.tsv | ./examples/word_count.rb --map | more
51
+
52
+ Next graduate to running @--run=local@ mode so you can inspect the reducer.
53
+
54
+ <notextile></div><div class="toggle"></notextile>
55
+
56
+ h2(#playnice). Wukong Plays nicely with others
57
+
58
+ Wukong is friends with "Hadoop":http://hadoop.apache.org/core the elephant, "Pig":http://hadoop.apache.org/pig/ the query language, and the @cat@ on your command line. It even has limited support for "martinis":http://datamapper.org (Datamapper) and "express trains":http://wiki.rubyonrails.org/rails/pages/ActiveRecord (ActiveRecord).
59
+
60
+ * "Export Wukong classes to SQL or Pig":#schema_export -- easily bulk-load and define SQL tables, or kickstart your pig scripts
61
+ * "Batch-Process records from ActiveRecord":#activerecord (the datamapper case is similar)
62
+ * Cascade Mappers and Reducers "purely in ruby":#stayinruby -- reportedly useful in an "ETL":http://en.wikipedia.org/wiki/Extract,_transform,_load context.
63
+
64
+ h3(#schema_export). Schema export to Pig or SQL
65
+
66
+ There is preliminary support for dumping wukong classes as schemata for other tools. For example, given the following:
67
+
68
+ {% highlight ruby %}
69
+ require "wukong" ;
70
+ require "wukong/schema"
71
+ User = TypedStruct.new(
72
+ [:id, Integer],
73
+ [:scraped_at, Bignum],
74
+ [:screen_name, String],
75
+ [:followers_count, Integer],
76
+ [:created_at, Bignum]
77
+ );
78
+ {% endhighlight %}
79
+
80
+ You can make a snippet for loading into pig with @puts User.load_pig@:
81
+
82
+ <pre> LOAD users.tsv AS ( rsrc:chararray, id: int, scraped_at: long, screen_name: chararray, followers_count: int, created_at: long )</pre>
83
+
84
+ Export to SQL with @puts User.sql_create_table ; puts User.sql_load_mysql@:
85
+
86
+ {% highlight sql %}
87
+ CREATE TABLE `users` (
88
+ `id` INT,
89
+ `scraped_at` BIGINT,
90
+ `screen_name` VARCHAR(255) CHARACTER SET ASCII,
91
+ `followers_count` INT,
92
+ `created_at` BIGINT
93
+ ) ;
94
+ ALTER TABLE `user` DISABLE KEYS;
95
+ LOAD DATA LOCAL INFILE 'user.tsv'
96
+ REPLACE INTO TABLE `user`
97
+ COLUMNS
98
+ TERMINATED BY '\t'
99
+ OPTIONALLY ENCLOSED BY ''
100
+ ESCAPED BY ''
101
+ LINES STARTING BY 'user'
102
+ ( @dummy,
103
+ `id`, `scraped_at`, `screen_name`, `followers_count`, `created_at`
104
+ );
105
+ ALTER TABLE `user` ENABLE KEYS ;
106
+ SELECT 'user', NOW(), COUNT(*) FROM `user`;
107
+ {% endhighlight %}
108
+
109
+ <notextile></div><div class="toggle"></notextile>
110
+
111
+ h2(#workflow). Wukong's internal workflow
112
+
113
+ Here's a somewhat detailed overview of a wukong script's internal workflow.
114
+
115
+ # You call @./myscript.rb --run infile outfile@
116
+ # Execution begins in the run method of the Script class (@wukong/script.rb@). It launches (depending on if you're local or remote) one of
117
+ ** @cat infile | ./myscript.rb --map | sort | ./myscript.rb --reduce > outfile@
118
+ ** @hadoop [a_crapton_of_streaming_args] -mapper './myscript.rb --map' -reducer './myscript.rb --reduce' @
119
+ # In either case, the effect is to spawn the exact same script you ran at the command line: one or more times with the --map command in place of the --run command, and one or more times with the --reduce command in place of the --run command. %(quiet)(well, unless you specify no reducers or a :map_command or something)%
120
+
121
+ # With the @--map@ or @--reduce@ flag given, the Script flag turns over control to the corresponding class: either @mapper_klass.new(self.options).stream@ or @reducer_klass.new(self.options).stream@
122
+
123
+ When in @--map@ or @--reduce@ mode (we'll just use @--map@ as an example):
124
+
125
+ # The mapper_klass is usually a subclass of @Streamer::Base@, but in actual fact it can be anything that initializes from a hash of options and responds to #stream.
126
+ # The default #stream method
127
+ ** calls the before_stream hook
128
+ ** reads each line from stdin ; #recordizes it ; passes it (if non-nil) to #process ; and emits each object yielded by #process
129
+ ** calls its after_stream hook
130
+ # You typically leave #stream alone and just override #process.
131
+ # The accumulator classes build on these patterns (they're proper subclasses of Streamer::Base), but are used differently. With an accumulator, you should implement some or all of
132
+ ** #start! -- called at the start of each accumulation, passing in the first record for that key
133
+ ** #accumulate -- called on each record (including that first one)
134
+ ** #finalize -- called when the last key of this accumulation is seen.
135
+ ** #get_key -- called on each record to recover its key.
136
+
137
+
138
+ h3(#stayinruby). Using wukong with internal streaming
139
+
140
+ If you're using wukong in local mode, you may not want to spawn new processes all over the place. Or your records may arrive not from the command line but from, say, a database call.
141
+
142
+ In that case, just override #stream. The original:
143
+
144
+ {% highlight ruby %}
145
+ #
146
+ # Pass each record to +#process+
147
+ #
148
+ def stream
149
+ before_stream
150
+ $stdin.each do |line|
151
+ record = recordize(line.chomp)
152
+ next unless record
153
+ process(*record) do |output_record|
154
+ emit output_record
155
+ end
156
+ end
157
+ after_stream
158
+ end
159
+ {% endhighlight %}
160
+
161
+ h3(#activerecord). Using wukong to Batch-Process ActiveRecord Objects
162
+
163
+ Here's a stream method, overridden to batch-process ActiveRecord objects (untested sample code):
164
+
165
+ {% highlight ruby %}
166
+ class Mapper < Wukong::Streamer
167
+ # Set record_klass to the ActiveRecord class you'd like to batch process
168
+ cattr_accessor :record_klass
169
+ # Size of each batch to pull from the database
170
+ cattr_accessor :batch_size
171
+
172
+ #
173
+ # Grab records from the database in batches,
174
+ # pass each record to +#process+
175
+ #
176
+ # Everything downstream of this is agnostic of the fact that
177
+ # records are coming from the database and not $stdin
178
+ #
179
+ def stream
180
+ before_stream
181
+ record_klass.find_in_batches(:batch_size => batch_size ) do |record_batch|
182
+ record_batch.each do |record|
183
+ process(record.id, record) do |output_record|
184
+ emit output_record
185
+ end
186
+ end
187
+ end
188
+ after_stream
189
+ end
190
+
191
+ # ....
192
+ end
193
+ {% endhighlight %}
194
+
195
+ <notextile></div></notextile>