newsman 1.0.0 → 1.2.2

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
  SHA256:
3
- metadata.gz: 3c6eaa01ebc8b603b13a1bb158e8a491ed2c51df32d973d56e9e99d959db7d4a
4
- data.tar.gz: be86c2c47cc64f2da87a04127cf879a8ccabfff8fa9557730e6f3031a225efdb
3
+ metadata.gz: '0166813d55623364efd38ff1f3bc83ff3ae76ee9d793b1b5ac52924477122135'
4
+ data.tar.gz: 48d3c00284b0973851b95c2b144fe50972f801deeb826b400ab31e87102b7cfc
5
5
  SHA512:
6
- metadata.gz: b2b8972c04260e5c588015270b0e192c47bc39cc72c52124b200b5150b16be0e0d149ebb85d890ebcf2e21d98168b220c49953e769b852a62f6d9e18af627a5c
7
- data.tar.gz: 12b435a44fb9687e9904f436f8b7203927856be57d1933045ca5fefbbe9d076346869962abd884c71e9bd6c2674daa1f9334186a90a84a79d2baa9b0c47d02a8
6
+ metadata.gz: 193cf28ea41b994cab278212cc255dcf0d1f7b78570e631f68e17003d489d505fd0e12a8b3244d576fa1e0826d992ead7af9c1febefd80074dc8260174294dae
7
+ data.tar.gz: 4e3a9c5920d471c3c5a1dcabe16692a6154ea50d44adaf775d263ce04186e814068391555d3e90682b043dbb2b3c4fb0372da2f8fa7e91abf331a594bfefe4e7
data/README.md CHANGED
@@ -62,7 +62,7 @@ Usage: newsman [options]
62
62
  -u, --username USERNAME GitHub username. For example, 'volodya-lombrozo'
63
63
  -r, --repository REPOSITORIES Specify which repositories to include in a report. You can specify several repositories using a comma separator, for example: '-r objectionary/jeo-maven-plugin,objectionary/opeo-maven-plugin'
64
64
  -p, --position POSITION Reporter position in a company. Default value is a 'Software Developer'.
65
- -o, --output OUTPUT Output type. Newsman prints a report to a stdout by default. You can choose another options like '-o html', '-o txt' or even '-o html'
65
+ -o, --output OUTPUT Output type. Newsman prints a report to a stdout by default. You can choose another options like '-o html', '-o txt' or '-o docx'. The 'html' output also generates a downloadable '.docx' copy (Times New Roman, 12pt) and links it from the page.
66
66
  -t, --title TITLE Project Title. Empty by default
67
67
  -m, --model MODEL AI model to use. gpt-3.5-turbo by default
68
68
  ```
@@ -98,6 +98,25 @@ newsman --help
98
98
  ```
99
99
  And you should see a welcome message from newsman.
100
100
 
101
+ ## Releases
102
+
103
+ Releases are handled by the [`release.yaml`](.github/workflows/release.yaml) GitHub Actions workflow. It runs whenever a tag matching `v*.*.*` (e.g. `v1.2.1`) is pushed, or manually via `workflow_dispatch`. On trigger it:
104
+
105
+ 1. Runs the test suite (`rake test`) and RuboCop (`rake rubocop`).
106
+ 2. Builds and installs the gem (`rake install`).
107
+ 3. Publishes the gem to [RubyGems.org](https://rubygems.org/gems/newsman) (`rake publish`), using the `RUBYGEMS_API_KEY` secret.
108
+ 4. Creates a GitHub Release for the pushed tag, using the `RELEASE_GITHUB_TOKEN` secret.
109
+
110
+ ### How to cut a release
111
+
112
+ 1. **Bump the version** in `newsman.gemspec` (`spec.version`) and commit it to `main`. RubyGems refuses to publish a version that's already live, so this step is mandatory - the workflow's `gem push` will silently fail otherwise (the publish step is `continue-on-error: true`, so the run can still show green even though nothing was actually published).
113
+ 2. **Tag the commit** with the same version, prefixed with `v`, and push the tag:
114
+ ```shell
115
+ git tag v1.2.1
116
+ git push origin v1.2.1
117
+ ```
118
+ 3. Watch the `Release Ruby Gem` workflow run in the Actions tab, and confirm the new version shows up on [RubyGems.org](https://rubygems.org/gems/newsman) and as a [GitHub Release](https://github.com/volodya-lombrozo/newsman/releases).
119
+
101
120
  ## Examples
102
121
 
103
122
  You can find examples of generated reports [here](https://volodya-lombrozo.github.io/newsman/)
@@ -112,32 +112,32 @@ class Assistant
112
112
  send(prompt)
113
113
  end
114
114
 
115
- # rubocop:disable Metrics/MethodLength
115
+ # OpenAI's reasoning-family models (o1, o3, o4-mini, gpt-5, ...) only accept
116
+ # the default temperature (1) and reject any explicit value with a 400.
117
+ # Their "-chat" flavors (e.g. gpt-5-chat-latest) are non-reasoning and
118
+ # support a custom temperature just like gpt-4o, gpt-3.5-turbo, etc.
119
+ FIXED_TEMPERATURE_MODELS = /\A(o[1-9]|gpt-5)/
120
+
116
121
  def send(request)
122
+ parameters = { model: @model, messages: messages(request) }
123
+ parameters[:temperature] = @temperature unless fixed_temperature?
124
+ @client.chat(parameters: parameters).dig('choices', 0, 'message', 'content')
125
+ end
126
+
127
+ def fixed_temperature?
128
+ @model.match?(FIXED_TEMPERATURE_MODELS) && !@model.include?('chat')
129
+ end
130
+
131
+ SYSTEM_PROMPT = 'You are a developer tasked with composing a concise report detailing'\
132
+ ' your activities and progress for the previous week, intended for submission to your supervisor.'
133
+
134
+ def messages(request)
117
135
  if @model == 'o1-preview'
118
- @client.chat(
119
- parameters: {
120
- model: @model,
121
- messages: [{ role: 'user', content: request.to_s }]
122
- }
123
- ).dig('choices', 0, 'message', 'content')
136
+ [{ role: 'user', content: request.to_s }]
124
137
  else
125
- @client.chat(
126
- parameters: {
127
- model: @model,
128
- messages: [
129
- { role: 'system', content: 'You are a developer tasked'\
130
- ' with composing a concise report detailing your activities'\
131
- ' and progress for the previous week,'\
132
- ' intended for submission to your supervisor.' },
133
- { role: 'user', content: request.to_s }
134
- ],
135
- temperature: @temperature
136
- }
137
- ).dig('choices', 0, 'message', 'content')
138
+ [{ role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: request.to_s }]
138
139
  end
139
140
  end
140
- # rubocop:enable Metrics/MethodLength
141
141
 
142
142
  def deprecated(method)
143
143
  warn "Warning! '#{method}' is deprecated and will be removed in future versions."
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2024 Volodya Lombrozo
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'zip'
24
+ require 'cgi'
25
+
26
+ # This class represents a report output in DOCX (MS Word) format.
27
+ # It builds a minimal, valid OOXML package by hand (no external docx-builder
28
+ # gem), so every run of text is explicitly styled with Times New Roman, 12pt.
29
+ class Docxout
30
+ # Content types part, required by every OOXML package.
31
+ CONTENT_TYPES = <<~XML
32
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
33
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>
34
+ XML
35
+
36
+ # Package-level relationships, pointing at the main document part.
37
+ RELS = <<~XML
38
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
39
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>
40
+ XML
41
+
42
+ FONT = 'Times New Roman'
43
+ # Word expresses font size in half-points, so 12pt is 24.
44
+ SIZE = '24'
45
+
46
+ def initialize(root = '.')
47
+ @root = root
48
+ end
49
+
50
+ def print(report, reporter, model)
51
+ puts "Create a docx file in a directory #{@root}"
52
+ path = File.join(@root, filename(reporter, model))
53
+ Zip::File.open(path, create: true) do |zip|
54
+ zip.get_output_stream('[Content_Types].xml') { |f| f.write(CONTENT_TYPES) }
55
+ zip.get_output_stream('_rels/.rels') { |f| f.write(RELS) }
56
+ zip.get_output_stream('word/document.xml') { |f| f.write(document_xml(report)) }
57
+ end
58
+ puts "Report was successfully printed to a #{path}"
59
+ File.basename(path)
60
+ end
61
+
62
+ def filename(reporter, model)
63
+ date = Time.new.strftime('%d.%m.%Y')
64
+ model = model.gsub('.', '-')
65
+ "#{date}.#{reporter}.#{model}.docx"
66
+ end
67
+
68
+ private
69
+
70
+ def document_xml(report)
71
+ body = paragraphs(report).map { |lines| paragraph_xml(lines) }.join
72
+ <<~XML
73
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
74
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>#{body}<w:sectPr/></w:body></w:document>
75
+ XML
76
+ end
77
+
78
+ def paragraphs(report)
79
+ report.to_s.split(/\n{2,}/).map { |paragraph| paragraph.split("\n") }
80
+ end
81
+
82
+ def paragraph_xml(lines)
83
+ runs = lines.each_with_index.map do |line, index|
84
+ "#{run_xml(line)}#{'<w:r><w:br/></w:r>' if index < lines.size - 1}"
85
+ end.join
86
+ "<w:p>#{runs}</w:p>"
87
+ end
88
+
89
+ def run_xml(line)
90
+ "<w:r><w:rPr><w:rFonts w:ascii=\"#{FONT}\" w:hAnsi=\"#{FONT}\" w:cs=\"#{FONT}\"/>" \
91
+ "<w:sz w:val=\"#{SIZE}\"/><w:szCs w:val=\"#{SIZE}\"/></w:rPr>" \
92
+ "<w:t xml:space=\"preserve\">#{CGI.escapeHTML(line)}</w:t></w:r>"
93
+ end
94
+ end
@@ -23,6 +23,7 @@
23
23
  require 'erb'
24
24
  require 'redcarpet'
25
25
  require 'nokogiri'
26
+ require_relative 'docx_output'
26
27
 
27
28
  # This class represents a report output in HTML format.
28
29
  class Htmlout
@@ -32,6 +33,7 @@ class Htmlout
32
33
  </head>
33
34
  <body>
34
35
  <h1><%= title %></h1>
36
+ <p><a href="<%= docx %>">Download as Word (.docx)</a></p>
35
37
  <%= body %>
36
38
  </body>
37
39
  HTML
@@ -44,6 +46,7 @@ class Htmlout
44
46
  def print(report, reporter, model)
45
47
  title = title(reporter)
46
48
  body = to_html(report)
49
+ docx = Docxout.new(@root).print(report, reporter, model)
47
50
  puts "Create a html file in a directory #{@root}"
48
51
  file = File.new(File.join(@root, filename(reporter, model)), 'w')
49
52
  puts "File #{file.path} was successfully created"
data/lib/newsman.rb CHANGED
@@ -31,6 +31,7 @@ require_relative 'newsman/issues'
31
31
  require_relative 'newsman/stdout_output'
32
32
  require_relative 'newsman/txt_output'
33
33
  require_relative 'newsman/html_output'
34
+ require_relative 'newsman/docx_output'
34
35
  require_relative 'newsman/report'
35
36
  require_relative 'newsman/assistant'
36
37
  require_relative 'newsman/github'
@@ -60,7 +61,7 @@ def generate
60
61
  end
61
62
  opts.on('-o', '--output OUTPUT',
62
63
  'Output type. Newsman prints a report to a stdout by default.'\
63
- "You can choose another options like '-o html', '-o txt' or even '-o html'") do |o|
64
+ "You can choose another options like '-o html', '-o txt' or '-o docx'") do |o|
64
65
  options[:output] = o
65
66
  end
66
67
  opts.on('-t', '--title TITLE', 'Project Title. Empty by default') { |t| options[:title] = t }
@@ -128,14 +129,19 @@ def generate
128
129
  full_answer = report.append_additional(full_answer)
129
130
  output_mode = options[:output]
130
131
  puts "Output mode is '#{output_mode}'"
131
- if output_mode.eql? 'txt'
132
+ case output_mode
133
+ when 'txt'
132
134
  puts 'Print result to a txt file'
133
135
  output = Txtout.new('.')
134
136
  output.print(full_answer, github_username)
135
- elsif output_mode.eql? 'html'
137
+ when 'html'
136
138
  puts 'Print result to a html file'
137
139
  output = Htmlout.new('.')
138
140
  output.print(full_answer, github_username, options[:model])
141
+ when 'docx'
142
+ puts 'Print result to a docx file'
143
+ output = Docxout.new('.')
144
+ output.print(full_answer, github_username, options[:model])
139
145
  else
140
146
  puts 'Print result to a stdout'
141
147
  output = Stdout.new
@@ -35,4 +35,31 @@ class TestAssistant < Minitest::Test
35
35
  assert_equal expected,
36
36
  assistant.say_hello
37
37
  end
38
+
39
+ def test_fixed_temperature_for_reasoning_models
40
+ %w[o1 o1-preview o1-mini o3 o3-mini o4-mini gpt-5 gpt-5-mini gpt-5.6].each do |model|
41
+ assistant = Assistant.new('test-token', model: model)
42
+ assert(assistant.fixed_temperature?, "expected #{model} to have a fixed temperature")
43
+ end
44
+ end
45
+
46
+ def test_custom_temperature_for_chat_models
47
+ %w[gpt-3.5-turbo gpt-4o gpt-4.1 gpt-5-chat-latest].each do |model|
48
+ assistant = Assistant.new('test-token', model: model)
49
+ refute(assistant.fixed_temperature?, "expected #{model} to allow a custom temperature")
50
+ end
51
+ end
52
+
53
+ def test_o1_preview_skips_system_message
54
+ assistant = Assistant.new('test-token', model: 'o1-preview')
55
+ messages = assistant.messages('do something')
56
+ assert_equal([{ role: 'user', content: 'do something' }], messages)
57
+ end
58
+
59
+ def test_other_models_keep_system_message
60
+ assistant = Assistant.new('test-token', model: 'gpt-5.6')
61
+ messages = assistant.messages('do something')
62
+ assert_equal(2, messages.size)
63
+ assert_equal('system', messages.first[:role])
64
+ end
38
65
  end
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright (c) 2024 Volodya Lombrozo
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the 'Software'), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+
24
+ require 'minitest/autorun'
25
+ require 'date'
26
+ require 'zip'
27
+ require_relative '../lib/newsman/docx_output'
28
+
29
+ class TestDocxout < Minitest::Test
30
+ def test_writes_to_a_docx_file
31
+ Dir.mktmpdir do |temp_dir|
32
+ output = Docxout.new(temp_dir)
33
+ today = Date.today.strftime('%d.%m.%Y')
34
+ expected = "#{today}.volodya-lombrozo.gpt-3-5-turbo.docx"
35
+ returned = output.print("Issue description\n\nHere is a new paragraph\nList is here:\n - one\n - two\n - three",
36
+ 'volodya-lombrozo',
37
+ 'gpt-3.5-turbo')
38
+ assert_equal(expected, returned)
39
+ assert(File.exist?(File.join(temp_dir, expected)))
40
+ end
41
+ end
42
+
43
+ def test_docx_uses_times_new_roman_12pt
44
+ Dir.mktmpdir do |temp_dir|
45
+ output = Docxout.new(temp_dir)
46
+ path = File.join(temp_dir, output.print('Issue description', 'volodya-lombrozo', 'gpt-3.5-turbo'))
47
+ document_xml = Zip::File.open(path) { |zip| zip.read('word/document.xml') }
48
+ assert_includes(document_xml, 'w:ascii="Times New Roman"')
49
+ assert_includes(document_xml, '<w:sz w:val="24"/>')
50
+ assert_includes(document_xml, 'Issue description')
51
+ end
52
+ end
53
+ end
data/test/test_htmlout.rb CHANGED
@@ -22,6 +22,7 @@
22
22
  # SOFTWARE.
23
23
 
24
24
  require 'minitest/autorun'
25
+ require 'date'
25
26
  require_relative '../lib/newsman/html_output'
26
27
 
27
28
  class TestHtmlout < Minitest::Test
@@ -32,6 +33,7 @@ class TestHtmlout < Minitest::Test
32
33
  </head>
33
34
  <body>
34
35
  <h1>volodya-lombrozo #{Time.new.strftime('%d.%m.%Y')}</h1>
36
+ <p><a href=\"#{Time.new.strftime('%d.%m.%Y')}.volodya-lombrozo.gpt-3-5-turbo.docx\">Download as Word (.docx)</a></p>
35
37
  <p>Issue description</p>
36
38
 
37
39
  <p>Here is a new paragraph<br/>
@@ -56,4 +58,14 @@ List is here:<br/>
56
58
  assert_equal(EXPECTED, File.read(File.join(temp_dir, expected)))
57
59
  end
58
60
  end
61
+
62
+ def test_also_writes_a_docx_file
63
+ Dir.mktmpdir do |temp_dir|
64
+ output = Htmlout.new(temp_dir)
65
+ today = Date.today.strftime('%d.%m.%Y')
66
+ expected = "#{today}.volodya-lombrozo.gpt-3-5-turbo.docx"
67
+ output.print('Issue description', 'volodya-lombrozo', 'gpt-3.5-turbo')
68
+ assert(File.exist?(File.join(temp_dir, expected)))
69
+ end
70
+ end
59
71
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: newsman
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Volodya Lombrozo
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: minitest
@@ -164,6 +163,20 @@ dependencies:
164
163
  - - "~>"
165
164
  - !ruby/object:Gem::Version
166
165
  version: '6.3'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rubyzip
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.3'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '2.3'
167
180
  description: A simple gem that gathers GitHub statistics and creates human-readable
168
181
  report
169
182
  email:
@@ -178,6 +191,7 @@ files:
178
191
  - bin/newsman
179
192
  - lib/newsman.rb
180
193
  - lib/newsman/assistant.rb
194
+ - lib/newsman/docx_output.rb
181
195
  - lib/newsman/github.rb
182
196
  - lib/newsman/html_output.rb
183
197
  - lib/newsman/issues.rb
@@ -186,6 +200,7 @@ files:
186
200
  - lib/newsman/stdout_output.rb
187
201
  - lib/newsman/txt_output.rb
188
202
  - test/test_assistant.rb
203
+ - test/test_docxout.rb
189
204
  - test/test_htmlout.rb
190
205
  - test/test_issue.rb
191
206
  - test/test_pdd_issue.rb
@@ -198,7 +213,6 @@ homepage: https://github.com/volodya-lombrozo/newsman
198
213
  licenses:
199
214
  - MIT
200
215
  metadata: {}
201
- post_install_message:
202
216
  rdoc_options: []
203
217
  require_paths:
204
218
  - lib
@@ -214,8 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
214
228
  - !ruby/object:Gem::Version
215
229
  version: '0'
216
230
  requirements: []
217
- rubygems_version: 3.5.22
218
- signing_key:
231
+ rubygems_version: 4.0.16
219
232
  specification_version: 4
220
233
  summary: GitHub user weekly news
221
234
  test_files: []