cl-magic 0.4.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,70 @@
1
+ require 'tty-progressbar'
2
+ require 'tty-spinner'
3
+ require 'concurrent'
1
4
 
2
5
  class Jira
3
6
 
7
+ MAX_THREADS = 20 # set to 1 to debug without concurrency
8
+
4
9
  def initialize(base_uri, username, token, break_at_one_page=false)
5
10
  @base_uri = base_uri.chomp("/")
6
11
  @username = username
7
12
  @token = token
8
13
  @break_at_one_page = break_at_one_page
14
+
15
+ @thread_pool = Concurrent::ThreadPoolExecutor.new(
16
+ min_threads: 0,
17
+ max_threads: MAX_THREADS,
18
+ max_queue: 0,
19
+ fallback_policy: :caller_runs
20
+ )
21
+ end
22
+
23
+ #
24
+ # Formatter
25
+ #
26
+
27
+ def self.jira_to_markdown(issue)
28
+
29
+ md = []
30
+ md << ""
31
+ md << "# #{issue['key']}"
32
+ md << "project: #{issue['fields']['project']['key']}"
33
+ md << "created: #{issue['fields']['created']}"
34
+ md << "updated: #{issue['fields']['updated']}"
35
+ md << "status: #{issue['fields']['status']['statusCategory']['name']}" unless issue['fields']["status"].nil?
36
+ md << "priority: #{issue['fields']['priority']['name']}"
37
+ md << "labels: #{issue['fields']['labels'].join(',')}"
38
+ md << "issue_type: #{issue['fields']['issuetype']['name']}" unless issue['fields']["issuetype"].nil?
39
+ md << "assignee: #{issue['fields']['assignee']['displayName']}" unless issue['fields']["assignee"].nil?
40
+ md << ""
41
+ md << "## Summary"
42
+ md << "#{issue['fields']['summary']}"
43
+ md << ""
44
+ md << ""
45
+ issue_md = md.join("\n")
46
+
47
+ comments = []
48
+ issue["comments"].each_with_index do |comment, i|
49
+ c_md = []
50
+ c_md << "### Comment - #{comment["author"]["displayName"]} "
51
+ c_md << ""
52
+ c_md << "created: #{comment["created"]}"
53
+
54
+ # nest markdown deeper
55
+ comment["body"].split("\n").each do |line|
56
+ c_md << if line.start_with?("#")
57
+ "####{line}"
58
+ else
59
+ line
60
+ end
61
+ end
62
+
63
+ c_md << ""
64
+ comments << [comment["id"], c_md.join("\n")]
65
+ end
66
+
67
+ return issue_md, comments
9
68
  end
10
69
 
11
70
  #
@@ -20,7 +79,7 @@ class Jira
20
79
  return epic_ids, epics
21
80
  end
22
81
 
23
- def get_issues(project, epic_ids)
82
+ def get_issues_by_epic_ids(project, epic_ids)
24
83
  jql_query = "project = \"#{project}\" AND parentEpic IN (#{epic_ids.join(',')})"
25
84
  return run_jql_query(jql_query)
26
85
  end
@@ -33,6 +92,14 @@ class Jira
33
92
  end
34
93
  end
35
94
 
95
+ def get_issue_comments(issue_key)
96
+ uri = URI.parse("#{@base_uri}/rest/api/2/issue/#{issue_key}/comment")
97
+ jira_get(uri) do |response|
98
+ result = JSON.parse(response.body)
99
+ return result["comments"]
100
+ end
101
+ end
102
+
36
103
  #
37
104
  # Helpers: GET & POST
38
105
  #
@@ -50,7 +117,11 @@ class Jira
50
117
  if response.code == '200'
51
118
  yield response
52
119
  else
53
- raise "Jira query failed with HTTP status code #{response.code}"
120
+ raise """
121
+ Jira query failed with HTTP status code #{response.code}
122
+
123
+ #{response.body}
124
+ """
54
125
  end
55
126
  end
56
127
 
@@ -69,7 +140,13 @@ class Jira
69
140
  if response.code == '200'
70
141
  yield response
71
142
  else
72
- raise "Jira query failed with HTTP status code #{response.code}"
143
+ raise """
144
+ Jira query failed with HTTP status code #{response.code}
145
+
146
+ BODY: #{body.to_json}
147
+
148
+ RESPONSE: #{response.body}
149
+ """
73
150
  end
74
151
  end
75
152
 
@@ -78,6 +155,9 @@ class Jira
78
155
  #
79
156
 
80
157
  def run_jql_query(jql)
158
+ spinner = TTY::Spinner.new("[:spinner] fetching ...", format: :pulse_2)
159
+ spinner.auto_spin # Automatic animation with default interval
160
+
81
161
  start_at = 0
82
162
  max_results = 50
83
163
  total_results = nil
@@ -111,53 +191,100 @@ class Jira
111
191
  start_at += max_results # else next page
112
192
  end
113
193
  end
114
-
115
- print '.' # loop
116
194
  end
195
+ spinner.stop("#{all_results.count} issues")
117
196
  all_results.map {|h| h}
118
197
  end
119
- end
120
198
 
121
- #
122
- # Collect status changelogs
123
- #
124
- # Given a array of jira issue hashes
125
- # * fetch the change log
126
- # * filter down to status changes
127
- # * add it to the issue hash as ["status_changelogs"]
128
- #
129
-
130
- def collect_status_changelogs(jira, issues, options)
131
- final_issue_hashes = []
132
-
133
- issues.each do |issue|
134
- issue_key = issue["key"]
135
- issue["status_changelogs"] = []
136
-
137
- # fetch change log
138
- print '.'
139
- changelogs = jira.get_issue_status_changelog(issue_key)
140
-
141
- changelogs.each do |change_log|
142
-
143
- # all items that are status changes
144
- status_logs = change_log["items"].select {|i| i["field"]=="status"}
145
- status_logs = status_logs.collect do |status_log|
146
- {
147
- "key": issue_key,
148
- "created": change_log["created"],
149
- "toString": status_log["toString"],
150
- "fromString": status_log["fromString"]
151
- }
199
+ def collect_comments(jira, issues)
200
+ final_issue_hashes = []
201
+ bar = TTY::ProgressBar.new("fetching [:bar]", total: issues.count)
202
+
203
+ issues.each do |issue|
204
+ do_concurently do
205
+ issue_key = issue["key"]
206
+ issue["comments"] = []
207
+
208
+ # fetch change log
209
+ comments = get_issue_comments(issue_key)
210
+ issue["comments"] = comments
211
+ final_issue_hashes << issue # save
212
+ bar.advance
213
+ end
214
+ end
215
+
216
+ # wait
217
+ wait_concurrently
218
+ return final_issue_hashes
219
+ end
220
+
221
+ #
222
+ # Collect status changelogs
223
+ #
224
+ # Given a array of jira issue hashes
225
+ # * fetch the change log
226
+ # * filter down to status changes
227
+ # * add it to the issue hash as ["status_changelogs"]
228
+ #
229
+
230
+ def collect_status_changelogs(jira, issues)
231
+ final_issue_hashes = []
232
+ bar = TTY::ProgressBar.new("fetching [:bar]", total: issues.count)
233
+
234
+ issues.each do |issue|
235
+ do_concurently do
236
+ issue_key = issue["key"]
237
+ issue["status_changelogs"] = []
238
+
239
+ # fetch change log
240
+ changelogs = get_issue_status_changelog(issue_key)
241
+
242
+ changelogs.each do |change_log|
243
+
244
+ # all items that are status changes
245
+ status_logs = change_log["items"].select {|i| i["field"]=="status"}
246
+ status_logs = status_logs.collect do |status_log|
247
+ {
248
+ "key": issue_key,
249
+ "created": change_log["created"],
250
+ "toString": status_log["toString"],
251
+ "fromString": status_log["fromString"]
252
+ }
253
+ end
254
+
255
+ # append them to issue
256
+ status_logs.each do |status_log|
257
+ issue["status_changelogs"] << status_log
258
+ end
259
+ end
260
+
261
+ final_issue_hashes << issue # save
262
+ bar.advance
152
263
  end
264
+ end
153
265
 
154
- # append them to issue
155
- status_logs.each do |status_log|
156
- issue["status_changelogs"] << status_log
266
+ # wait
267
+ wait_concurrently
268
+ return final_issue_hashes
269
+ end
270
+
271
+ private
272
+
273
+ def do_concurently
274
+ if MAX_THREADS > 1
275
+ @thread_pool.post do
276
+ yield
157
277
  end
278
+ else
279
+ yield
158
280
  end
281
+ end
159
282
 
160
- final_issue_hashes << issue # save
283
+ def wait_concurrently
284
+ if MAX_THREADS > 1
285
+ @thread_pool.shutdown
286
+ @thread_pool.wait_for_termination
287
+ end
161
288
  end
162
- return final_issue_hashes
289
+
163
290
  end
@@ -0,0 +1,78 @@
1
+
2
+ class Milvus
3
+ def initialize(host, port)
4
+ @host = host
5
+ @port = port
6
+ end
7
+
8
+ def search(collection_name, embedding)
9
+ final_url = "http://#{@host}:#{@port}/v1/vector/search"
10
+ data = {
11
+ collectionName: collection_name,
12
+ vector: embedding,
13
+ outputFields: ["id", "name", "doc_key", "distance"],
14
+ }
15
+
16
+ # post
17
+ sanitized_data = data.to_json
18
+ cmd = """
19
+ curl -s \
20
+ '#{final_url}' \
21
+ -X 'POST' \
22
+ -H 'accept: application/json' \
23
+ -H 'Content-Type: application/json' \
24
+ -d '#{sanitized_data}'
25
+ """
26
+ return `#{cmd}`
27
+ end
28
+
29
+ def create_collection(collection_name)
30
+ final_url = "http://#{@host}:#{@port}/v1/vector/collections/create"
31
+ data = {
32
+ dbName: "default",
33
+ collectionName: collection_name,
34
+ dimension: 1536,
35
+ metricType: "L2",
36
+ primaryField: "id",
37
+ vectorField: "vector"
38
+ }
39
+
40
+ # post
41
+ sanitized_data = data.to_json
42
+ cmd = """
43
+ curl -s \
44
+ '#{final_url}' \
45
+ -X 'POST' \
46
+ -H 'accept: application/json' \
47
+ -H 'Content-Type: application/json' \
48
+ -d '#{sanitized_data}'
49
+ """
50
+ return `#{cmd}`
51
+ end
52
+
53
+ def post_to_collection(collection_name, doc_key, embedding)
54
+ final_url = "http://#{@host}:#{@port}/v1/vector/insert"
55
+ data = {
56
+ collectionName: collection_name,
57
+ data: {
58
+ doc_key: doc_key,
59
+ vector: embedding
60
+ }
61
+ }
62
+
63
+ # post
64
+ sanitized_data = data.to_json
65
+ cmd = """
66
+ curl -s \
67
+ '#{final_url}' \
68
+ -X POST \
69
+ -H 'accept: application/json' \
70
+ -H 'Content-Type: application/json' \
71
+ -d '#{sanitized_data}'
72
+ """
73
+ response = `#{cmd}`
74
+ data = JSON.parse(response)
75
+ raise "Error: #{data.to_json}\n\nData #{sanitized_data}" if data.has_key?("message")
76
+ return data.to_json
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+
2
+ class HelpPrinter
3
+
4
+ def initialize(logger)
5
+ @logger = logger
6
+ end
7
+
8
+ def print_dk_help_line(key, help)
9
+ if $stdout.isatty
10
+ if help.nil?
11
+ @logger.puts("#{key.ljust(15, ' ')} ???no help???")
12
+ else
13
+ key = key.ljust(15, ' ')
14
+ help_parts = help.split(";")
15
+
16
+ # first line
17
+ @logger.puts(key, help_parts.shift)
18
+
19
+ # following lines
20
+ padding = "".ljust(15, ' ')
21
+ help_parts.each do |p|
22
+ @logger.puts(padding, p)
23
+ end
24
+ @logger.puts("") if help.end_with?(";")
25
+ end
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,67 @@
1
+ require 'tty-command'
2
+ require 'yaml'
3
+
4
+ class PartsMerger
5
+
6
+ def initialize(working_dir, yaml_arg_munger, help_printer, logger)
7
+ @working_dir = working_dir
8
+ @yaml_arg_munger = yaml_arg_munger
9
+ @help_printer = help_printer
10
+ @logger = logger
11
+ end
12
+
13
+ def merge_parts(compose_hash, dk_parts_hash, args)
14
+ selected_part_keys = []
15
+
16
+ # merge: saved parts
17
+ saved_part_keys = get_saved_parts(dk_parts_hash)
18
+ saved_part_keys.each do |potential_part_key|
19
+ dk_part = dk_parts_hash.fetch(potential_part_key, nil) # yml detail
20
+ if dk_part
21
+ compose_hash = print_and_merge_part(potential_part_key, dk_part, compose_hash)
22
+ selected_part_keys << potential_part_key
23
+ end
24
+ end
25
+
26
+ # merge: arg parts
27
+ while true
28
+ potential_part_key = args.first
29
+ dk_part = dk_parts_hash.fetch(potential_part_key, nil) # yml detail
30
+ if dk_part
31
+ unless selected_part_keys.include?(potential_part_key)
32
+ compose_hash = print_and_merge_part(potential_part_key, dk_part, compose_hash)
33
+ selected_part_keys << potential_part_key
34
+ end
35
+ args.shift # remove part arg
36
+ else
37
+ break
38
+ end
39
+ end
40
+ @logger.puts "" if $stdout.isatty # tailing line break
41
+
42
+ return compose_hash, selected_part_keys, args
43
+ end
44
+
45
+ def get_saved_parts(dk_parts_hash)
46
+
47
+ # get saved parts
48
+ cmd = "cd #{@working_dir} && cl dk parts list"
49
+ out, err = TTY::Command.new(:printer => :null).run("#{cmd}")
50
+ return out.split("\n")
51
+
52
+ end
53
+
54
+ private
55
+
56
+ def print_and_merge_part(part_key, dk_part, compose_hash)
57
+
58
+ # print
59
+ if $stdout.isatty
60
+ help_str = dk_part.fetch('help')
61
+ @help_printer.print_dk_help_line("#{part_key}", "#{help_str ? '- ' + help_str : ''}") if dk_part.keys.any?
62
+ end
63
+ # merge
64
+ return @yaml_arg_munger.dk_merge_and_remove(compose_hash, dk_part)
65
+ end
66
+
67
+ end
@@ -0,0 +1,52 @@
1
+ require 'tty-command'
2
+ require 'yaml'
3
+
4
+ class WorldSettings
5
+
6
+ def initialize(working_dir)
7
+ @working_dir = working_dir
8
+ end
9
+
10
+ def get_world_settings_hash()
11
+ filepath = get_world_settings_filepath()
12
+ return File.exist?(filepath) ? YAML.load_file(filepath) : {}
13
+ end
14
+
15
+ def save_world_settings(world_settings_hash)
16
+ filepath = get_world_settings_filepath()
17
+ tempfile = File.new(filepath, 'w')
18
+ tempfile.write(world_settings_hash.to_yaml)
19
+ tempfile.close
20
+ end
21
+
22
+ def get_world_project_path()
23
+ repo_basename = get_repo_basename()
24
+ world_path = get_world_path_from_settings()
25
+ return File.join(world_path, repo_basename) if world_path and repo_basename
26
+ return nil
27
+ end
28
+
29
+ def get_world_path_from_settings()
30
+ world_settings = get_world_settings_hash()
31
+ if world_settings.key?(:world_path) and world_settings.key?(:context)
32
+ return File.join(world_settings[:world_path], world_settings[:context])
33
+ end
34
+ return ""
35
+ end
36
+
37
+ private
38
+
39
+ def get_repo_basename()
40
+ command = "cd #{@working_dir} && basename $(git remote get-url origin 2> /dev/null) .git"
41
+ repo_basename = TTY::Command.new(:printer => :null).run(command).out.gsub('.git', '').strip.chomp
42
+ if repo_basename==".git" or repo_basename==""
43
+ return File.basename(@working_dir)
44
+ end
45
+ return repo_basename
46
+ end
47
+
48
+ def get_world_settings_filepath()
49
+ return File.join(".cl-dk-world.yml")
50
+ end
51
+
52
+ end
@@ -0,0 +1,107 @@
1
+ require 'yaml'
2
+
3
+ class YamlArgMunger
4
+
5
+ def initialize(working_dir, world_settings)
6
+ @working_dir = working_dir
7
+ @world_path = world_settings.get_world_path_from_settings()
8
+ @dk_proj_path = world_settings.get_world_project_path()
9
+ end
10
+
11
+ def get_base_compose_parts_and_make_hashes()
12
+ compose_hash = get_base_compose_hash()
13
+ dk_parts_hash = {}
14
+ dk_make_hash = {}
15
+ if compose_hash
16
+ compose_hash = merge_world_files(compose_hash, show_help=ARGV.include?("--help"))
17
+ dk_parts_hash = compose_hash['x-dk-parts'] ? compose_hash.delete('x-dk-parts') : {}
18
+ dk_make_hash = compose_hash['x-dk-make'] ? compose_hash.delete('x-dk-make') : {}
19
+ end
20
+ return compose_hash, dk_parts_hash, dk_make_hash
21
+ end
22
+
23
+ def dk_merge_and_remove(compose_hash, yml_hash)
24
+ # remove help & merge
25
+ clean_yml = yml_hash.clone
26
+ clean_yml.delete('help')
27
+ return compose_hash.dk_merge(clean_yml).dk_reject! { |k, v| v=='<dk-remove>' }
28
+ end
29
+
30
+ def save_yaml_and_adjust_args(compose_hash, args)
31
+
32
+ # generate final compose file
33
+ tempfile = File.new(File.join(@working_dir, ".cl-dk.yml"), 'w')
34
+ tempfile.write(compose_hash.to_yaml) # write it to the tempfile
35
+
36
+ # remove existing '-f' flag, if needed
37
+ file_flag_index = args.index('-f')
38
+ if file_flag_index==0
39
+ args.delete_at(file_flag_index)
40
+ args.delete_at(file_flag_index)
41
+ end
42
+ args.unshift('-f', tempfile.path) # add new '-f' flag
43
+
44
+ tempfile.close
45
+ return args
46
+ end
47
+
48
+ private
49
+
50
+ def get_base_compose_hash()
51
+ cmd = "cd #{@working_dir} && docker compose config 2> /dev/null"
52
+ return YAML.load(`#{cmd}`)
53
+ end
54
+
55
+ def merge_world_files(compose_hash, show_help=false)
56
+ if @dk_proj_path
57
+ print_dk_help_line("dk-project-path", "#{@dk_proj_path}") if show_help and $stdout.isatty
58
+
59
+ Dir.glob("#{@dk_proj_path}/*.yml").sort.each do |filepath|
60
+ print_dk_help_line("dk-world", "#{filepath}") if show_help and $stdout.isatty
61
+
62
+ # read file and replace
63
+ contents = File.read(filepath)
64
+ contents.gsub!('<dk-world-path>', @world_path)
65
+ contents.gsub!('<dk-project-path>', @dk_proj_path)
66
+ contents.gsub!('<dk-working-path>', @working_dir)
67
+
68
+ # yml merge
69
+ yml_hash = YAML.load(contents)
70
+ compose_hash = dk_merge_and_remove(compose_hash, yml_hash)
71
+ end
72
+ end
73
+ return compose_hash
74
+ end
75
+
76
+ class ::Hash
77
+ def dk_merge(second)
78
+ merger = proc { |_, v1, v2|
79
+ if Hash === v1 && Hash === v2
80
+ v1.merge(v2, &merger)
81
+ else
82
+ if Array === v1 && Array === v2
83
+ if v2.first=="<dk-replace>"
84
+ v2[1..-1] # everything but the first item
85
+ else
86
+ v1 | v2 # union arrays
87
+ end
88
+ else
89
+ if [:undefined, nil, :nil].include?(v2)
90
+ v1
91
+ else
92
+ v2
93
+ end
94
+ end
95
+ end
96
+ }
97
+ merge(second.to_h, &merger)
98
+ end
99
+ def dk_reject!(&blk)
100
+ self.each do |k, v|
101
+ v.dk_reject!(&blk) if v.is_a?(Hash)
102
+ self.delete(k) if blk.call(k, v)
103
+ end
104
+ end
105
+ end
106
+
107
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Cl
4
4
  module Magic
5
- VERSION = "0.4.0"
5
+ VERSION = "1.2.0"
6
6
  end
7
7
  end