phraseapp_updater 0.1.7 → 2.0.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.
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'multi_json'
2
4
  require 'oj'
3
5
 
4
6
  # We're working with pure JSON, not
5
7
  # serialized Ruby objects
6
- Oj.default_options = {mode: :strict}
8
+ Oj.default_options = { mode: :strict }
7
9
 
8
10
  class PhraseAppUpdater
9
11
  class LocaleFile
@@ -11,21 +13,26 @@ class PhraseAppUpdater
11
13
  EXTENSION = "json"
12
14
  PHRASEAPP_TYPE = "nested_json"
13
15
 
14
- def self.from_hash(name, hash)
15
- new(name, MultiJson.dump(hash))
16
- end
16
+ class << self
17
+ def load(content)
18
+ Oj.load(content)
19
+ rescue Oj::ParseError => e
20
+ raise ArgumentError.new("Provided content was not valid JSON: #{e}")
21
+ end
17
22
 
18
- def parse(content)
19
- MultiJson.load(content)
20
- rescue MultiJson::ParseError => e
21
- raise ArgumentError.new("Provided content was not valid JSON: #{e}")
22
- end
23
+ def dump(hash)
24
+ # Add indentation for better diffs
25
+ Oj.dump(hash, indent: 2, mode: :strict)
26
+ end
23
27
 
24
- def format_content!
25
- # Add indentation for better diffs
26
- @content = MultiJson.dump(MultiJson.load(@content), pretty: true)
28
+ def extension
29
+ EXTENSION
30
+ end
31
+
32
+ def phraseapp_type
33
+ PHRASEAPP_TYPE
34
+ end
27
35
  end
28
36
  end
29
37
  end
30
38
  end
31
-
@@ -5,17 +5,24 @@ class PhraseAppUpdater
5
5
  EXTENSION = "yml"
6
6
  PHRASEAPP_TYPE = "yml"
7
7
 
8
- def self.from_hash(name, hash)
9
- new(name, Psych.dump(hash))
10
- end
8
+ class << self
9
+ def load(content)
10
+ Psych.load(content)
11
+ rescue Psych::SyntaxError => e
12
+ raise ArgumentError.new("Provided content was not valid YAML")
13
+ end
11
14
 
12
- def parse(content)
13
- Psych.load(content)
14
- rescue Psych::SyntaxError => e
15
- raise ArgumentError.new("Provided content was not valid YAML")
16
- end
15
+ def dump(hash)
16
+ Psych.dump(hash)
17
+ end
18
+
19
+ def extension
20
+ EXTENSION
21
+ end
17
22
 
18
- def format_content!
23
+ def phraseapp_type
24
+ PHRASEAPP_TYPE
25
+ end
19
26
  end
20
27
  end
21
28
  end
@@ -1,44 +1,100 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'phraseapp_updater/locale_file'
4
+ require 'phraseapp_updater/index_by'
2
5
  require 'phraseapp-ruby'
3
- require 'thread'
6
+ require 'parallel'
4
7
  require 'tempfile'
5
8
 
6
9
  class PhraseAppUpdater
10
+ using IndexBy
7
11
  class PhraseAppAPI
12
+ GIT_TAG_PREFIX = 'gitancestor_'
13
+
8
14
  def initialize(api_key, project_id, locale_file_class)
9
15
  @client = PhraseApp::Client.new(PhraseApp::Auth::Credentials.new(token: api_key))
10
16
  @project_id = project_id
11
17
  @locale_file_class = locale_file_class
12
18
  end
13
19
 
14
- def download_locales
15
- # This is a paginated API, however the maximum page size of 100
16
- # is well above our expected locale size,
17
- # so we take the first page only for now
20
+ def create_project(name, parent_commit)
21
+ params = PhraseApp::RequestParams::ProjectParams.new(
22
+ name: name,
23
+ main_format: @locale_file_class.phraseapp_type)
24
+ project = phraseapp_request { @client.project_create(params) }
25
+ STDERR.puts "Created project #{name} for #{parent_commit}"
26
+
27
+ @project_id = project.id
28
+ store_parent_commit(parent_commit)
29
+
30
+ project.id
31
+ end
32
+
33
+ # We mark projects with their parent git commit using a tag with a
34
+ # well-known prefix. We only allow one tag with this prefix at once.
35
+ def read_parent_commit
36
+ tags = phraseapp_request { @client.tags_list(@project_id, 1, 100) }
37
+ git_tag = tags.detect { |t| t.name.start_with?(GIT_TAG_PREFIX) }
38
+ raise MissingGitParentError.new if git_tag.nil?
39
+
40
+ git_tag.name.delete_prefix(GIT_TAG_PREFIX)
41
+ end
42
+
43
+ def update_parent_commit(commit_hash)
44
+ previous_parent = read_parent_commit
45
+ phraseapp_request do
46
+ @client.tag_delete(@project_id, GIT_TAG_PREFIX + previous_parent)
47
+ end
48
+ store_parent_commit(commit_hash)
49
+ end
50
+
51
+ def fetch_locales
52
+ # This is a paginated API, however the maximum page size of 100 is well
53
+ # above our expected locale size, so we take the first page only for now
18
54
  phraseapp_request { @client.locales_list(@project_id, 1, 100) }.map do |pa_locale|
19
55
  Locale.new(pa_locale)
20
56
  end
21
57
  end
22
58
 
23
- def download_files(locales, skip_unverified)
24
- threaded_request(locales) do |locale|
25
- puts "Downloading file for #{locale}"
59
+ def create_locale(name, default: false)
60
+ phraseapp_request do
61
+ params = PhraseApp::RequestParams::LocaleParams.new(
62
+ name: name,
63
+ code: name,
64
+ default: default,
65
+ )
66
+ @client.locale_create(@project_id, params)
67
+ end
68
+ end
69
+
70
+ def download_files(locales, skip_unverified:)
71
+ results = threaded_request(locales) do |locale|
72
+ STDERR.puts "Downloading file for #{locale}"
26
73
  download_file(locale, skip_unverified)
27
- end.map do |locale, file_contents|
28
- @locale_file_class.new(locale.name, file_contents)
74
+ end
75
+
76
+ locales.zip(results).map do |locale, file_contents|
77
+ @locale_file_class.from_file_content(locale.name, file_contents)
29
78
  end
30
79
  end
31
80
 
32
- def upload_files(locale_files)
81
+ def upload_files(locale_files, default_locale:)
82
+ known_locales = fetch_locales.index_by(&:name)
83
+
33
84
  threaded_request(locale_files) do |locale_file|
34
- puts "Uploading #{locale_file}"
85
+ unless known_locales.has_key?(locale_file.locale_name)
86
+ create_locale(locale_file.locale_name,
87
+ default: (locale_file.locale_name == default_locale))
88
+ end
89
+
90
+ STDERR.puts "Uploading #{locale_file}"
35
91
  upload_file(locale_file)
36
- end.map { |locale_file, upload_id | upload_id }
92
+ end
37
93
  end
38
94
 
39
95
  def remove_keys_not_in_uploads(upload_ids)
40
96
  threaded_request(upload_ids) do |upload_id|
41
- puts "Removing keys not in upload #{upload_id}"
97
+ STDERR.puts "Removing keys not in upload #{upload_id}"
42
98
  remove_keys_not_in_upload(upload_id)
43
99
  end
44
100
  end
@@ -46,18 +102,18 @@ class PhraseAppUpdater
46
102
  def download_file(locale, skip_unverified)
47
103
  download_params = PhraseApp::RequestParams::LocaleDownloadParams.new
48
104
 
49
- download_params.file_format = @locale_file_class.const_get(:PHRASEAPP_TYPE)
105
+ download_params.file_format = @locale_file_class.phraseapp_type
50
106
  download_params.skip_unverified_translations = skip_unverified
51
107
 
52
108
  phraseapp_request { @client.locale_download(@project_id, locale.id, download_params) }
53
109
  end
54
110
 
55
111
  def upload_file(locale_file)
56
- upload_params = create_upload_params(locale_file.name)
112
+ upload_params = create_upload_params(locale_file.locale_name)
57
113
 
58
114
  # The PhraseApp gem only accepts a filename to upload,
59
115
  # so we need to write the file out and pass it the path
60
- Tempfile.create(["#{locale_file.name}", ".json"]) do |f|
116
+ Tempfile.create([locale_file.locale_name, ".json"]) do |f|
61
117
  f.write(locale_file.content)
62
118
  f.close
63
119
 
@@ -72,7 +128,7 @@ class PhraseAppUpdater
72
128
 
73
129
  begin
74
130
  phraseapp_request { @client.keys_delete(@project_id, delete_params) }
75
- rescue RuntimeError => e
131
+ rescue RuntimeError => _e
76
132
  # PhraseApp will accept but mark invalid uploads, however the gem
77
133
  # returns the same response in both cases. If we call this API
78
134
  # with the ID of an upload of a bad file, it will fail.
@@ -84,65 +140,51 @@ class PhraseAppUpdater
84
140
 
85
141
  private
86
142
 
143
+ def store_parent_commit(commit_hash)
144
+ params = PhraseApp::RequestParams::TagParams.new(
145
+ name: GIT_TAG_PREFIX + commit_hash)
146
+ phraseapp_request { @client.tag_create(@project_id, params) }
147
+ end
148
+
87
149
  def phraseapp_request(&block)
88
- begin
89
- res, err = block.call
90
- rescue RuntimeError => e
91
- if e.message[/\(401\)/]
92
- raise BadAPIKeyError.new(e)
93
- elsif e.message[/not found/]
94
- raise BadProjectIDError.new(e)
95
- else
96
- raise e
97
- end
98
- end
150
+ res, err = block.call
99
151
 
100
152
  unless err.nil?
101
- if err.respond_to?(:error)
102
- error = err.error
103
- else
104
- error = err.errors.join("|")
105
- end
153
+ error =
154
+ if err.respond_to?(:error)
155
+ err.error
156
+ else
157
+ err.errors.join('|')
158
+ end
106
159
 
107
160
  raise RuntimeError.new(error)
108
161
  end
109
162
 
110
163
  res
164
+
165
+ rescue RuntimeError => e
166
+ if e.message.match?(/\(401\)/)
167
+ raise BadAPIKeyError.new(e)
168
+ elsif e.message.match?(/not found/)
169
+ raise BadProjectIDError.new(e, @project_id)
170
+ elsif e.message.match?(/has already been taken/)
171
+ raise ProjectNameTakenError.new(e)
172
+ else
173
+ raise
174
+ end
111
175
  end
112
176
 
113
177
  # PhraseApp allows two concurrent connections at a time.
114
178
  THREAD_COUNT = 2
115
179
 
116
180
  def threaded_request(worklist, &block)
117
- queue = worklist.inject(Queue.new, :push)
118
- threads = []
119
-
120
- THREAD_COUNT.times do
121
- threads << Thread.new do
122
- Thread.current[:result] = {}
123
-
124
- begin
125
- while work = queue.pop(true) do
126
- Thread.current[:result][work] = block.call(work)
127
- end
128
- rescue ThreadError => e
129
- Thread.exit
130
- end
131
-
132
- end
133
- end
134
-
135
- threads.each(&:join)
136
-
137
- threads.each_with_object({}) do |thread, results|
138
- results.merge!(thread[:result])
139
- end
181
+ Parallel.map(worklist, in_threads: THREAD_COUNT, &block)
140
182
  end
141
183
 
142
184
  def create_upload_params(locale_name)
143
185
  upload_params = PhraseApp::RequestParams::UploadParams.new
144
- upload_params.file_encoding = "UTF-8"
145
- upload_params.file_format = @locale_file_class.const_get(:PHRASEAPP_TYPE)
186
+ upload_params.file_encoding = 'UTF-8'
187
+ upload_params.file_format = @locale_file_class.phraseapp_type
146
188
  upload_params.locale_id = locale_name
147
189
  upload_params.skip_unverification = false
148
190
  upload_params.update_translations = true
@@ -171,6 +213,12 @@ class PhraseAppUpdater
171
213
  end
172
214
  end
173
215
 
216
+ class MissingGitParentError < RuntimeError
217
+ def initialize
218
+ super('Could not locate tag representing git ancestor commit')
219
+ end
220
+ end
221
+
174
222
  class BadAPIKeyError < RuntimeError
175
223
  def initialize(original_error)
176
224
  super(original_error.message)
@@ -178,10 +226,18 @@ class PhraseAppUpdater
178
226
  end
179
227
 
180
228
  class BadProjectIDError < RuntimeError
229
+ attr_reader :project_id
230
+
231
+ def initialize(original_error, id)
232
+ @project_id = id
233
+ super(original_error.message)
234
+ end
235
+ end
236
+
237
+ class ProjectNameTakenError < RuntimeError
181
238
  def initialize(original_error)
182
239
  super(original_error.message)
183
240
  end
184
241
  end
185
242
  end
186
243
  end
187
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class PhraseAppUpdater
2
- VERSION = "0.1.7"
4
+ VERSION = "2.0.0"
3
5
  end
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "multi_json", "~> 1.12"
26
26
  spec.add_dependency "oj", "~> 2.18"
27
27
  spec.add_dependency "deep_merge", "~> 1.1"
28
+ spec.add_dependency "parallel", "~> 1.12"
28
29
 
29
30
  spec.add_development_dependency "bundler", "~> 1.12"
30
31
  spec.add_development_dependency "rake", "~> 10.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phraseapp_updater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Griffin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-12 00:00:00.000000000 Z
11
+ date: 2018-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '1.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: parallel
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.12'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.12'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: bundler
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -168,14 +182,15 @@ files:
168
182
  - README.markdown
169
183
  - Rakefile
170
184
  - bin/console
185
+ - bin/phraseapp_common.sh
171
186
  - bin/phraseapp_updater
172
187
  - bin/setup
188
+ - bin/synchronize_phraseapp.sh
173
189
  - lib/phraseapp_updater.rb
174
190
  - lib/phraseapp_updater/differ.rb
175
191
  - lib/phraseapp_updater/index_by.rb
176
192
  - lib/phraseapp_updater/locale_file.rb
177
193
  - lib/phraseapp_updater/locale_file/json_file.rb
178
- - lib/phraseapp_updater/locale_file/loader.rb
179
194
  - lib/phraseapp_updater/locale_file/yaml_file.rb
180
195
  - lib/phraseapp_updater/phraseapp_api.rb
181
196
  - lib/phraseapp_updater/version.rb
@@ -201,7 +216,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
216
  version: '0'
202
217
  requirements: []
203
218
  rubyforge_project:
204
- rubygems_version: 2.6.11
219
+ rubygems_version: 2.7.7
205
220
  signing_key:
206
221
  specification_version: 4
207
222
  summary: A three-way differ for PhraseApp projects.
@@ -1,24 +0,0 @@
1
- require 'phraseapp_updater/locale_file'
2
-
3
- class PhraseAppUpdater
4
- class LocaleFile
5
- class Loader
6
- def initialize(extension)
7
- @extension = extension
8
- end
9
-
10
- def load(filename)
11
- unless File.readable?(filename) && File.file?(filename)
12
- raise RuntimeError.new("Couldn't read localization file at #{filename}")
13
- end
14
-
15
- LocaleFile.class_for_file_format(@extension).new(File.basename(filename).chomp(".#{@extension}"), File.read(filename))
16
- end
17
-
18
- def filenames(locale_directory)
19
- Dir["#{locale_directory}/*.#{@extension}"]
20
- end
21
- end
22
- end
23
- end
24
-