pt 0.8.6 → 0.9.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93ae006e38a2336e7717d7754d1ff07e7e86162e
4
- data.tar.gz: a6f6d11a0f765bb8862b486ac9b96cd3facab79f
3
+ metadata.gz: 1c872c7de07ff476f016e21cd526a26cde532c61
4
+ data.tar.gz: 2b5cd00d28ad94fcf20842d6c50b9eb02bf4ef8d
5
5
  SHA512:
6
- metadata.gz: 739d0a6c02367b3808f71ea867b92cd2937cfcb47a50b429ddace71c7511886fab9aad6e52ef0363dd31b1a500c54e7aa90472d5cc9a5e4bd5b6a97273f67db2
7
- data.tar.gz: f283c929e0a18a1be42cd80c9db675be318654e91b0e8a6024e23446564de17a5483b3a1fccaed2ce674d00b7e98a6ff9d249aebe0042e074d6b37ac6497c913
6
+ metadata.gz: dae2f14490c2b022db21fc2f5408e29365907091b0f41992aa65577f2aeac8b037f41d4487459042ec05aaedba4489c67ae184391b31f571c8abe137de328535
7
+ data.tar.gz: eccedbf39e375f0389423657e0bf9abab46600211ed377735e91a4b364ac2798422a5269dfc1eebdb44efa246c48f031ff6ba6cefec078d26ab6ca66bccee6a6
@@ -1,5 +1,9 @@
1
1
  # pt changelog
2
2
 
3
+ ## v0.9.0
4
+ - add pagination for long stories (limit: default is 20, can be changed by set :limit: on local .pt file)
5
+ -
6
+
3
7
  ## v0.8.6
4
8
  - replace ruby save navigation operator
5
9
  ## v0.8.5
data/Gemfile CHANGED
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in pt.gemspec
4
4
  gemspec
5
+ gem 'aruba', '~> 0.14.2'
data/README.md CHANGED
@@ -15,54 +15,11 @@ The first time you run it, `pt` will ask you some data about your Pivotal Tracke
15
15
 
16
16
  ## Usage
17
17
 
18
- Run `pt` from the root folder of your project.
19
-
20
- ```
21
- pt # show all available stories
22
-
23
- pt todo <owner> # show all unscheduled stories
24
-
25
- pt (unscheduled,started,finished,delivered, accepted, rejected) <owner> # show all (unscheduled,started,finished,delivered, accepted, rejected) stories
26
-
27
- pt create [title] <owner> <type> -m # create a new story (and include description ala git commit)
28
-
29
- pt show [id] # shows detailed info about a story
30
-
31
- pt tasks [id] # manage tasks of story
32
-
33
- pt open [id] # open a story in the browser
34
-
35
- pt assign [id] <owner> # assign owner
36
-
37
- pt comment [id] [comment] # add a comment
38
-
39
- pt label [id] [label] # add a label
40
-
41
- pt estimate [id] [0-3] # estimate a story in points scale
42
-
43
- pt (start,finish,deliver,accept) [id] # mark a story as started
44
-
45
- pt reject [id] [reason] # mark a story as rejected, explaining why
46
-
47
- pt done [id] <0-3> <comment> # lazy mans finish story, opens, assigns to you, estimates, finish & delivers
48
-
49
- pt find [query] # looks in your stories by title and presents it
50
-
51
- pt list [owner] # list all stories for another pt user
52
-
53
- pt list all # list all stories for all users
54
-
55
- pt updates # shows number recent activity from your current project
56
-
57
- pt recent # shows stories you've recently shown or commented on with pt
58
-
59
- All commands can be run entirely without arguments for a wizard based UI. Otherwise [required] <optional>.
60
- Anything that takes an id will also take the num (index) from the pt command.
61
- ```
18
+ Run `pt` from the root folder of your project will show all available command.
62
19
 
63
20
  ## Problems?
64
21
 
65
- [Open a new issue](https://github.com/raul/pt/issues/new). It can be helpful to include a trace of the requests and responses you're getting from Pivotal Tracker: you can get it by adding the `--debug` parameter while invoking `ptt` (remember to remove all sensible data though).
22
+ [Open a new issue](https://github.com/raul/pt/issues/new).
66
23
 
67
24
  ## Contribute
68
25
  - `git clone https://github.com/raul/pt.git`
data/bin/pt CHANGED
@@ -7,4 +7,4 @@ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
7
7
 
8
8
  require 'pt'
9
9
 
10
- PT::UI.new ARGV
10
+ PT::CLI.start ARGV
data/lib/pt.rb CHANGED
@@ -3,7 +3,9 @@ require "pt/version"
3
3
  require 'pt/client'
4
4
  require 'pt/data_row'
5
5
  require 'pt/data_table'
6
- require 'pt/ui'
6
+ require 'pt/action'
7
+ require 'pt/helper'
8
+ require 'pt/cli'
7
9
 
8
10
  module PT
9
11
  # Your code goes here...
@@ -0,0 +1,151 @@
1
+ module PT
2
+ module Action
3
+ def show_story(story)
4
+ title('========================================='.red)
5
+ title story.name.red
6
+ title('========================================='.red)
7
+ estimation = [-1, nil].include?(story.estimate) ? "Unestimated" : "#{story.estimate} points"
8
+ requester = story.requested_by ? story.requested_by.initials : @local_config[:user_name]
9
+ message "#{story.current_state.capitalize} #{story.story_type} | #{estimation} | Req: #{requester} | Owners: #{story.owners.map(&:initials).join(',')} | ID: #{story.id}"
10
+
11
+ if story.labels.present?
12
+ message "Labels: " + story.labels.map(&:name).join(', ')
13
+ end
14
+ message story.description.green unless story.description.nil? || story.description.empty?
15
+ message "View on pivotal: #{story.url}"
16
+
17
+ if story.tasks.present?
18
+ title('tasks'.yellow)
19
+ story.tasks.each{ |t| compact_message "- #{t.complete ? "[done]" : ""} #{t.description}" }
20
+ end
21
+
22
+
23
+ story.comments.each do |n|
24
+ title('......................................'.blue)
25
+ text = ">> #{n.person.initials}: #{n.text}"
26
+ text << "[#{n.file_attachment_ids.size}F]" if n.file_attachment_ids
27
+ message text
28
+ end
29
+ save_recent_task( story.id )
30
+ end
31
+
32
+ def tasks_story(story)
33
+ story_task = get_open_story_task_from_params(story)
34
+ if story_task.position == -1
35
+ description = ask('Title for new task')
36
+ story.create_task(:description => description)
37
+ congrats("New todo task added to \"#{story.name}\"")
38
+ else
39
+ edit_story_task story_task
40
+ end
41
+ end
42
+
43
+ def open_story story
44
+ `open #{story.url}`
45
+ end
46
+
47
+ def assign_story story
48
+ if (owner = find_owner @params[1])
49
+ @client.assign_task(story, owner)
50
+ else
51
+ members = @client.get_members
52
+ table = PersonsTable.new(members.map(&:person))
53
+ owner = select("Please select a member to assign him the story", table)
54
+ end
55
+
56
+ congrats("story assigned to #{owner.initials}, thanks!")
57
+ end
58
+
59
+ def comment_story(story)
60
+ comment = @params[1] || ask("Write your comment")
61
+ if @client.comment_task(story, comment)
62
+ congrats("Comment sent, thanks!")
63
+ save_recent_task( story.id )
64
+ else
65
+ error("Ummm, something went wrong.")
66
+ end
67
+ end
68
+
69
+ def label_story(story)
70
+ if @params[1]
71
+ label = @params[1]
72
+ else
73
+ label = ask("Which label?")
74
+ end
75
+
76
+ @client.add_label(story, label );
77
+ show_story(task_by_id_or_pt_id(story.id))
78
+ end
79
+
80
+ def estimate_story(story)
81
+ estimation ||= ask("How many points you estimate for it? (#{project.point_scale})")
82
+ @client.estimate_story(story, estimation)
83
+ congrats("Task estimated, thanks!")
84
+ end
85
+
86
+ def start_story story
87
+ @client.mark_task_as(story, 'started')
88
+ congrats("story started, go for it!")
89
+ end
90
+
91
+ def finish_story story
92
+ if story.story_type == 'chore'
93
+ @client.mark_task_as(story, 'accepted')
94
+ else
95
+ @client.mark_task_as(story, 'finished')
96
+ end
97
+ congrats("Another story bites the dust, yeah!")
98
+ end
99
+
100
+ def deliver_story story
101
+ return if story.story_type == 'chore'
102
+ @client.mark_task_as(story, 'delivered')
103
+ congrats("story delivered, congrats!")
104
+ end
105
+
106
+ def accept_story story
107
+ @client.mark_task_as(story, 'accepted')
108
+ congrats("Accepted")
109
+ end
110
+
111
+ def reject_story(story)
112
+ comment = @params[1] || ask("Please explain why are you rejecting the story.")
113
+ if @client.comment_task(story, comment)
114
+ @client.mark_task_as(story, 'rejected')
115
+ congrats("story rejected, thanks!")
116
+ else
117
+ error("Ummm, something went wrong.")
118
+ end
119
+ end
120
+
121
+ def done_story(story)
122
+ #we need this for finding again later
123
+ story_id = story.id
124
+
125
+ if !@params[1] && story.estimate == -1
126
+ error("You need to give an estimate for this task")
127
+ return
128
+ end
129
+
130
+ if @params[1] && story.estimate == -1
131
+ if [0,1,2,3].include? @params[1].to_i
132
+ estimate_story(story, @params[1].to_i)
133
+ end
134
+ if @params[2]
135
+ story = task_by_id_or_pt_id story_id
136
+ @client.comment_task(story, @params[2])
137
+ end
138
+ else
139
+ @client.comment_task(story, @params[1]) if @params[1]
140
+ end
141
+
142
+ start_story story
143
+
144
+ finish_story story
145
+
146
+ deliver_story story
147
+ end
148
+
149
+ end
150
+ end
151
+
@@ -0,0 +1,183 @@
1
+ require 'yaml'
2
+ require 'colored'
3
+ require 'highline'
4
+ require 'tempfile'
5
+ require 'uri'
6
+ require 'thor'
7
+ module PT
8
+ class CLI < Thor
9
+ include PT::Action
10
+ include PT::Helper
11
+ attr_reader :project
12
+
13
+ def initialize(*args)
14
+ super
15
+ @io = HighLine.new
16
+ @global_config = load_global_config
17
+ @local_config = load_local_config
18
+ @client = Client.new(@global_config[:api_number], @local_config)
19
+ @project = @client.project
20
+ end
21
+
22
+ %w[unscheduled started finished delivered accepted rejected].each do |state|
23
+ desc "#{state} <owner>", "show all #{state} stories"
24
+ define_method(state.to_sym) do |owner = nil|
25
+ filter = "state:#{state}"
26
+ filter << " owner:#{owner}" if owner
27
+ story = select_story_from_paginated_table do |page|
28
+ @client.get_stories(filter: filter, page: page)
29
+ end
30
+ show_story(story)
31
+ end
32
+ end
33
+
34
+ %w[show tasks open assign comments label estimate start finish deliver accept reject done].each do |action|
35
+ desc "#{action} [id]", "#{action} story"
36
+ define_method(action.to_sym) do |story_id = nil|
37
+ if story_id
38
+ story = task_by_id_or_pt_id(story_id.to_i)
39
+ unless story
40
+ message("No matches found for '#{story_id}', please use a valid pivotal story Id")
41
+ return
42
+ end
43
+ else
44
+ method_name = "get_stories_to_#{action}"
45
+ story = select_story_from_paginated_table do |page|
46
+ if @client.respond_to?(method_name.to_sym)
47
+ @client.send("get_stories_to_#{action}", page: page)
48
+ else
49
+ @client.get_stories(page: page)
50
+ end
51
+ end
52
+ end
53
+ title("#{action} '#{story.name}'")
54
+ send("#{action}_story", story)
55
+ end
56
+ end
57
+
58
+ desc 'mywork', 'list all your stories'
59
+ def mywork
60
+ story = select_story_from_paginated_table do |page|
61
+ @client.get_stories(filter: "owner:#{@local_config[:user_name]} -state:accepted", page: page)
62
+ end
63
+ show_story(story)
64
+ end
65
+
66
+ desc "list [owner]", "list all stories from owner"
67
+ def list(owner = nil)
68
+ if owner
69
+ if owner == "all"
70
+ stories = @client.get_work
71
+ TasksTable.new(stories).print @global_config
72
+ else
73
+ stories = @client.get_my_work(owner)
74
+ TasksTable.new(stories).print @global_config
75
+ end
76
+ else
77
+ members = @client.get_members
78
+ table = MembersTable.new(members)
79
+ user = select("Please select a member to see his tasks.", table).name
80
+ title("Work for #{user} in #{project_to_s}")
81
+ stories = @client.get_my_work(user)
82
+ TasksTable.new(stories).print @global_config
83
+ end
84
+ end
85
+
86
+ desc "recent", "show stories you've recently shown or commented on with pt"
87
+ def recent
88
+ title("Your recent stories from #{project_to_s}")
89
+ stories = @project.stories( ids: @local_config[:recent_tasks].join(',') )
90
+ MultiUserTasksTable.new(stories).print @global_config
91
+ end
92
+
93
+ desc 'create [title] --owner <owner> --type <type> -m', "create a new story (and include description ala git commit)"
94
+ long_desc <<-LONGDESC
95
+ create story with title [title]
96
+
97
+ --owner, -o set owner
98
+
99
+ --type , -t set story type
100
+
101
+ -m enable add description using vim
102
+
103
+ omit all parameters will start interactive mode
104
+ LONGDESC
105
+ option :type, aliases: :t
106
+ option :owner, aliases: :o
107
+ option :m, type: :boolean
108
+ def create(title =nil)
109
+ owner = options[:owner]
110
+ type = options[:type]
111
+ requester_id = @local_config[:user_id]
112
+ if title
113
+ name = title
114
+ owner = owner || @local_config[:user_name]
115
+ type = task_type_or_nil(owner) || task_type_or_nil(type) || 'feature'
116
+ else
117
+ title("Let's create a new task:")
118
+ name = ask("Name for the new task:")
119
+ end
120
+
121
+ owner = @client.find_member(owner).person.id if owner.kind_of?(String)
122
+
123
+ unless owner
124
+ if ask('Do you want to assign it now? (y/n)').downcase == 'y'
125
+ members = @client.get_members
126
+ table = PersonsTable.new(members.map(&:person))
127
+ owner = select("Please select a member to assign him the task.", table).id
128
+ else
129
+ owner = nil
130
+ end
131
+ requester = @local_config[:user_name]
132
+ type = ask('Type? (c)hore, (b)ug, anything else for feature)')
133
+ end
134
+
135
+ type = case type
136
+ when 'c', 'chore'
137
+ 'chore'
138
+ when 'b', 'bug'
139
+ 'bug'
140
+ else
141
+ 'feature'
142
+ end
143
+
144
+ owner_ids = [owner]
145
+ # did you do a -m so you can add a description?
146
+ if options[:m]
147
+ editor = ENV.fetch('EDITOR') { 'vi' }
148
+ temp_path = "/tmp/editor-#{ Process.pid }.txt"
149
+ system "#{ editor } #{ temp_path }"
150
+
151
+ description = File.read(temp_path)
152
+ end
153
+
154
+ story = @client.create_story(
155
+ name: name,
156
+ owner_ids: owner_ids,
157
+ requested_by_id: requester_id,
158
+ story_type: type,
159
+ description: description
160
+ )
161
+ congrats("#{type} for #{owner} open #{story.url}")
162
+ show_story(story)
163
+ end
164
+
165
+ desc "find [query] " ,"looks in your stories by title and presents it"
166
+ def find(query)
167
+ story = select_story_from_paginated_table do |page|
168
+ @client.get_stories(filter: query, page: page)
169
+ end
170
+ show_story(story)
171
+ end
172
+
173
+ desc "updates","shows number recent activity from your current project"
174
+ def updates
175
+ activities = @client.get_activities
176
+ tasks = @client.get_my_work
177
+ title("Recent Activity on #{project_to_s}")
178
+ activities.each do |activity|
179
+ show_activity(activity, tasks)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -7,6 +7,7 @@ module PT
7
7
 
8
8
  STORY_FIELDS=':default,requested_by,owners,tasks,comments(:default,person,file_attachments)'
9
9
 
10
+ attr_reader :config, :project
10
11
 
11
12
  def self.get_api_token(email, password)
12
13
  PivotalAPI::Me.retrieve(email, password)
@@ -14,9 +15,10 @@ module PT
14
15
  raise PT::InputError.new("Bad email/password combination.")
15
16
  end
16
17
 
17
- def initialize(token)
18
+ def initialize(token, local_config=nil)
18
19
  @client = TrackerApi::Client.new(token: token)
19
- @project = nil
20
+ @config = local_config
21
+ @project = @client.project(local_config[:project_id]) if local_config
20
22
  end
21
23
 
22
24
  def get_project(project_id)
@@ -28,7 +30,7 @@ module PT
28
30
  @client.projects
29
31
  end
30
32
 
31
- def get_membership(project, email)
33
+ def get_membership(email)
32
34
  PivotalTracker::Membership.all(project).select{ |m| m.email == email }.first
33
35
  end
34
36
 
@@ -40,117 +42,125 @@ module PT
40
42
  PivotalTracker::Iteration.current(project)
41
43
  end
42
44
 
43
- def get_activities(project, limit)
45
+ def get_activities
44
46
  project.activity
45
47
  end
46
48
 
47
- def get_work(project)
49
+ def get_work
48
50
  project.stories(filter: 'state:unscheduled,unstarted,started', fields: STORY_FIELDS )
49
51
  end
50
52
 
51
- def get_my_work(project, user_name)
52
- project.stories(filter: "owner:#{user_name} -state:accepted", limit: 50, fields: STORY_FIELDS)
53
+ def get_my_work
54
+ project.stories(filter: "owner:#{config[:user_name]} -state:accepted", limit: 50, fields: STORY_FIELDS)
53
55
  end
54
56
 
55
- def search_for_story(project, query)
56
- project.stories(filter: query.to_s ,fields: STORY_FIELDS)
57
+ def search_for_story(query, params={})
58
+ params[:filter] = "#{query}"
59
+ get_stories(params)
57
60
  end
58
61
 
59
- def get_task_by_id(project, id)
62
+ def get_task_by_id(id)
60
63
  project.story(id, fields: STORY_FIELDS)
61
64
  end
62
65
  alias :get_story :get_task_by_id
63
66
 
64
- def get_my_open_tasks(project, user_name)
65
- project.stories filter: "owner:#{user_name}", fields: STORY_FIELDS
67
+ def get_my_open_tasks(params={})
68
+ params[:filter] = "owner:#{config[:user_name]}"
69
+ get_stories(params)
66
70
  end
67
71
 
68
- def get_my_tasks_to_estimate(project, user_name)
69
- project.stories( filter: "owner:#{user_name} type:feature estimate:-1", fields: STORY_FIELDS)
72
+ def get_stories_to_estimate(params={})
73
+ params[:filter] = "owner:#{config[:user_name]} type:feature estimate:-1"
74
+ get_stories(params)
70
75
  end
71
76
 
72
- def get_my_tasks_to_start(project, user_name)
73
- tasks = project.stories filter: "owner:#{user_name} state:unscheduled,rejected,unstarted", limit: 50, fields: STORY_FIELDS
74
- tasks.reject{ |t| (t.story_type == 'feature') && (t.estimate == -1) }
77
+ def get_stories_to_start(params={})
78
+ params[:filter] = "owner:#{config[:user_name]} type:feature,bug state:unscheduled,rejected,unstarted"
79
+ tasks = get_stories(params)
80
+ tasks.reject{ |t| (t.story_type == 'feature') && (!t.estimate) }
75
81
  end
76
82
 
77
- def get_my_tasks_to_finish(project, user_name)
78
- project.stories filter: "owner:#{user_name} -state:finished,delivered,accepted,rejected", limit: 50, fields: STORY_FIELDS
83
+ def get_stories_to_finish(params={})
84
+ params[:filter] = "owner:#{config[:user_name]} -state:unscheduled,rejected"
85
+ get_stories(params)
79
86
  end
80
87
 
81
- def get_my_tasks_to_deliver(project, user_name)
82
- project.stories filter: "owner:#{user_name} -state:delivered,accepted,rejected", limit: 50, fields: STORY_FIELDS
88
+ def get_stories_to_deliver(params={})
89
+ params[:filter] = "owner:#{config[:user_name]} -state:delivered,accepted,rejected"
90
+ get_stories(params)
83
91
  end
84
92
 
85
- def get_my_tasks_to_accept(project, user_name)
86
- project.stories filter: "owner:#{user_name} -state:accepted", limit: 50, fields: STORY_FIELDS
93
+ def get_stories_to_accept(params={})
94
+ params[:filter] = "owner:#{config[:user_name]} -state:accepted"
95
+ get_stories(params)
87
96
  end
88
97
 
89
- def get_my_tasks_to_reject(project, user_name)
90
- project.stories filter: "owner:#{user_name} -state:rejected", limit: 50, fields: STORY_FIELDS
98
+ def get_stories_to_reject(params={})
99
+ params[:filter] = "owner:#{config[:user_name]} -state:rejected"
100
+ get_stories(params)
91
101
  end
92
102
 
93
- def get_tasks_to_assign(project)
94
- project.stories filter: "-state:accepted", limit: 50
103
+ def get_stories_to_assign(params={})
104
+ params[:filter] = "-state:accepted"
105
+ get_stories(params)
95
106
  end
96
107
 
97
- def get_all_stories(project, user_name)
98
- project.stories limit: 20, fields: STORY_FIELDS
108
+ def get_stories(params={})
109
+ limit = config[:limit] || 20
110
+ page = params[:page] || 0
111
+ offset = page*limit
112
+ filter = params[:filter] || '-state=accepted'
113
+ project.stories limit: limit, fields: STORY_FIELDS, auto_paginate: false, offset: offset, filter: filter
99
114
  end
100
115
 
101
116
 
102
- def get_member(project, query)
117
+ def get_member(query)
103
118
  member = project.memberships.select{ |m| m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase }
104
119
  member.empty? ? nil : member.first
105
120
  end
106
121
 
107
- def find_member(project, query)
108
- memberships = project.memberships.detect do |m|
122
+ def find_member(query)
123
+ project.memberships.detect do |m|
109
124
  m.person.name.downcase.start_with?(query.downcase) || m.person.initials.downcase == query.downcase
110
125
  end
111
126
  end
112
127
 
113
- def get_members(project)
128
+ def get_members
114
129
  project.memberships fields: ':default,person'
115
130
  end
116
131
 
117
132
 
118
- def mark_task_as(project, task, state)
119
- task = get_story(project, task.id)
133
+ def mark_task_as(task, state)
134
+ task = get_story(task.id)
120
135
  task.current_state = state
121
136
  task.save
122
137
  end
123
138
 
124
- def estimate_task(project, task, points)
125
- task = get_story(project, task.id)
139
+ def estimate_story(task, points)
140
+ task = get_story(task.id)
126
141
  task.estimate = points
127
142
  task.save
128
143
  end
129
144
 
130
- def assign_task(project, task, owner)
131
- task = get_story(project, task.id)
145
+ def assign_task(task, owner)
146
+ task = get_story(task.id)
132
147
  task.add_owner(owner)
133
148
  end
134
149
 
135
- def add_label(project, task, label)
136
- task = get_story(project, task.id)
150
+ def add_label(task, label)
151
+ task = get_story(task.id)
137
152
  task.add_label(label)
138
153
  task.save
139
154
  end
140
155
 
141
- def comment_task(project, task, comment)
142
- task = get_story(project, task.id)
156
+ def comment_task(task, comment)
157
+ task = get_story(task.id)
143
158
  task.create_comment(text: comment)
144
159
  end
145
160
 
146
- def create_task(project, name, owner_ids, task_type)
147
- project.create_story(:name => name, :story_type => task_type, owner_ids: owner_ids)
161
+ def create_story(args)
162
+ project.create_story(args)
148
163
  end
149
164
 
150
- def create_task_with_description(project, name, owner, task_type, description)
151
- project.create_story(:name => name, :story_type => task_type, :description => description)
152
- end
153
-
154
-
155
165
  end
156
166
  end