bas 0.1.0 → 0.2.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
  SHA256:
3
- metadata.gz: 9d8ff973fc126ff9d0af5adf9200417f302a9a86cc50be3d3ef090a8979a22b1
4
- data.tar.gz: 846dfe3cd6d719aa0a7cc17ce2219a0dee54a5d89043a475ec93b4786fd688d2
3
+ metadata.gz: 97cc25416cff1a50cad38e85bbc1e7389693ccf2f4004a1c5fb8d5049162b407
4
+ data.tar.gz: efef410e0d776c8808655dbd20cb7500c81bcf53c16f072a7103d5827def9e58
5
5
  SHA512:
6
- metadata.gz: 14ea01f630b4a296fa94a352d14bc00111833d2f5b1867861280c5aa32abca445bdc7ec888c83a4a673109aba2c5e3239fdccc48cc7315477a3b579d200b649c
7
- data.tar.gz: 3b73cf8b1dc7d931c3cd428141f2a3806c1b90c23bb1c0ac2dc755bf16278b679f42405f4d504b5cfbd6d1f09787d065f34fe3a06cee15ed9d684a93d2249971
6
+ metadata.gz: f7d915c41461667d858750e117e63468fba9cdea4fac59dfd80b0dcf1a7c4ce749b773378b06a81e2cdfafbefce92a61a2ee8415a80edd6a1e1a61380c5bdda9
7
+ data.tar.gz: fa174b7e86b013055efabe32b7dc7c2f213bd8dd18600c35e655d4c7512ce8e8f7ed4e104c091ca5b4def7180d4499a1c37f3dd5f4ad5e0a936ac79d716f03b1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (04.04.2024)
4
+ - [Add a GitHub fetcher component](https://github.com/kommitters/bas/issues/6)
5
+ - [Allow daily hours pto for more than one day pto](https://github.com/kommitters/bas/issues/8)
6
+
3
7
  ## 0.1.0 (26.03.2024)
4
8
  - [Add the BNS gem code](https://github.com/kommitters/bas/issues/1)
data/Gemfile CHANGED
@@ -5,10 +5,16 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in bas.gemspec
6
6
  gemspec
7
7
 
8
+ gem "jwt", "~> 2.8.1"
9
+
8
10
  gem "rake", "~> 13.0"
9
11
 
10
12
  gem "net-imap", "~> 0.4.10"
11
13
  gem "net-smtp", "~> 0.4.0.1"
14
+
15
+ gem "octokit", "~> 8.1.0"
16
+ gem "openssl", "~> 3.2"
17
+
12
18
  gem "rspec", "~> 3.0"
13
19
  gem "rubocop", "~> 1.21"
14
20
  gem "simplecov", require: false, group: :test
data/README.md CHANGED
@@ -6,11 +6,11 @@ The underlying idea is to develop generic components that can serve a wide range
6
6
 
7
7
  ![Gem Version](https://img.shields.io/gem/v/bas?style=for-the-badge)
8
8
  ![Gem Total Downloads](https://img.shields.io/gem/dt/bas?style=for-the-badge)
9
- ![Build Badge](https://img.shields.io/github/actions/workflow/status/kommitters/bas/ci.yml?branch=project-opensource-config&style=for-the-badge)
9
+ ![Build Badge](https://img.shields.io/github/actions/workflow/status/kommitters/bas/ci.yml?style=for-the-badge)
10
10
  [![Coverage Status](https://img.shields.io/coveralls/github/kommitters/bas?style=for-the-badge)](https://coveralls.io/github/kommitters/bas?branch=main)
11
11
  ![GitHub License](https://img.shields.io/github/license/kommitters/bas?style=for-the-badge)
12
12
  [![OpenSSF Scorecard](https://img.shields.io/ossf-scorecard/github.com/kommitters/bas?label=openssf%20scorecard&style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/kommitters/bas)
13
- [![OpenSSF Best Practices](https://img.shields.io/cii/summary/8383?label=openssf%20best%20practices&style=for-the-badge)](https://bestpractices.coreinfrastructure.org/projects/8383)
13
+ [![OpenSSF Best Practices](https://img.shields.io/cii/summary/8713?label=openssf%20best%20practices&style=for-the-badge)](https://bestpractices.coreinfrastructure.org/projects/8713)
14
14
 
15
15
  ## Installation
16
16
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domain
4
+ ##
5
+ # The Domain::Issue class provides a domain-specific representation of a Github issue object.
6
+ # It encapsulates information about a repository issue, including the title, state, assignees,
7
+ # description, and the repository url.
8
+ #
9
+ class Issue
10
+ attr_reader :title, :state, :assignees, :description, :url
11
+
12
+ ATTRIBUTES = %w[title state assignees description url].freeze
13
+
14
+ def initialize(title, state, assignees, body, url)
15
+ @title = title
16
+ @state = state
17
+ @assignees = assignees
18
+ @description = body
19
+ @url = url
20
+ end
21
+ end
22
+ end
@@ -7,9 +7,9 @@ module Domain
7
7
  # the start date, and the end date of the time off period.
8
8
  #
9
9
  class Pto
10
- attr_reader :individual_name, :start_date, :end_date
10
+ attr_reader :individual_name, :start_date_from, :start_date_to, :end_date_from, :end_date_to
11
11
 
12
- ATTRIBUTES = %w[individual_name start_date end_date].freeze
12
+ ATTRIBUTES = %w[individual_name start_date_from start_date_to end_date_from end_date_to].freeze
13
13
 
14
14
  # Initializes a Domain::Pto instance with the specified individual name, start date, and end date.
15
15
  #
@@ -21,8 +21,49 @@ module Domain
21
21
  #
22
22
  def initialize(individual_name, start_date, end_date)
23
23
  @individual_name = individual_name
24
- @start_date = start_date
25
- @end_date = end_date
24
+
25
+ @start_date_from = start_date[:from]
26
+ @start_date_to = start_date[:to]
27
+ @end_date_from = end_date[:from]
28
+ @end_date_to = end_date[:to]
29
+ end
30
+
31
+ def same_day?
32
+ start_date = extract_date(start_date_from)
33
+ end_date = extract_date(end_date_from)
34
+
35
+ start_date == end_date
36
+ end
37
+
38
+ def format_timezone(timezone)
39
+ @start_date_from = set_timezone(start_date_from, timezone)
40
+ @start_date_to = set_timezone(start_date_to, timezone)
41
+ @end_date_from = set_timezone(end_date_from, timezone)
42
+ @end_date_to = set_timezone(end_date_to, timezone)
43
+ end
44
+
45
+ private
46
+
47
+ def extract_date(date)
48
+ return if date.nil?
49
+
50
+ date.strftime("%F")
51
+ end
52
+
53
+ def build_date_time(date, timezone)
54
+ return if date.nil?
55
+
56
+ date_time = date.include?("T") ? date : "#{date}T00:00:00.000#{timezone}"
57
+
58
+ DateTime.parse(date_time).to_time
59
+ end
60
+
61
+ def set_timezone(date, timezone)
62
+ return if date.nil?
63
+
64
+ date_time = build_date_time(date, timezone)
65
+
66
+ Time.at(date_time, in: timezone)
26
67
  end
27
68
  end
28
69
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "octokit"
4
+ require "openssl"
5
+ require "jwt"
6
+
7
+ require_relative "../base"
8
+ require_relative "./types/response"
9
+
10
+ module Fetcher
11
+ module Github
12
+ ##
13
+ # This class is an implementation of the Fetcher::Base interface, specifically designed
14
+ # for fetching data from a GitHub repository.
15
+ #
16
+ class Base < Fetcher::Base
17
+ protected
18
+
19
+ # Implements the data fetching logic to get data from a Github repository.
20
+ # It connects to Github using the octokit gem, authenticates with a github app,
21
+ # request the data and returns a validated response.
22
+ #
23
+ def execute(method, *filter)
24
+ octokit_response = octokit.public_send(method, *filter)
25
+
26
+ Fetcher::Github::Types::Response.new(octokit_response)
27
+ end
28
+
29
+ private
30
+
31
+ def octokit
32
+ Octokit::Client.new(bearer_token: access_token)
33
+ end
34
+
35
+ def access_token
36
+ app = Octokit::Client.new(client_id: config[:app_id], bearer_token: jwt)
37
+
38
+ app.create_app_installation_access_token(config[:installation_id])[:token]
39
+ end
40
+
41
+ def jwt
42
+ private_pem = File.read(config[:secret_path])
43
+ private_key = OpenSSL::PKey::RSA.new(private_pem)
44
+
45
+ JWT.encode(jwt_payload, private_key, "RS256")
46
+ end
47
+
48
+ def jwt_payload
49
+ {
50
+ iat: Time.now.to_i - 60,
51
+ exp: Time.now.to_i + (10 * 60),
52
+ iss: config[:app_id]
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Github
5
+ module Types
6
+ ##
7
+ # Represents a response received from the Octokit Github client. It encapsulates essential
8
+ # information about the response, providing a structured way to handle and analyze
9
+ # it's responses.
10
+ class Response
11
+ attr_reader :status_code, :message, :results
12
+
13
+ def initialize(response)
14
+ if response.empty?
15
+ @status_code = 404
16
+ @message = "no result were found"
17
+ @results = []
18
+ else
19
+ @status_code = 200
20
+ @message = "success"
21
+ @results = response
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module Fetcher
6
+ module Github
7
+ ##
8
+ # This class is an implementation of the Fetcher::Github::Base interface, specifically designed
9
+ # for fetching issues from a Github repository.
10
+ #
11
+ class RepoIssues < Github::Base
12
+ def fetch
13
+ execute("list_issues", config[:repo])
14
+ end
15
+ end
16
+ end
17
+ end
@@ -38,6 +38,8 @@ module Formatter
38
38
  def format(ptos_list)
39
39
  raise Formatter::Exceptions::InvalidData unless ptos_list.all? { |pto| pto.is_a?(Domain::Pto) }
40
40
 
41
+ ptos_list.each { |pto| pto.format_timezone(@timezone) }
42
+
41
43
  ptos_list.reduce("") do |payload, pto|
42
44
  built_template = build_template(Domain::Pto::ATTRIBUTES, pto)
43
45
  payload + format_message_by_case(built_template.gsub("\n", ""), pto)
@@ -47,42 +49,37 @@ module Formatter
47
49
  private
48
50
 
49
51
  def format_message_by_case(built_template, pto)
50
- date_start = format_timezone(pto.start_date).strftime("%F")
51
- date_end = format_timezone(pto.end_date).strftime("%F")
52
-
53
- if date_start == date_end
52
+ if pto.same_day?
54
53
  interval = same_day_interval(pto)
55
- day_message = today?(date_start) ? "today" : "the day #{date_start}"
54
+ day_message = today?(pto.start_date_from) ? "today" : "the day #{pto.start_date_from.strftime("%F")}"
56
55
 
57
56
  "#{built_template} #{day_message} #{interval}\n"
58
57
  else
59
- "#{built_template} from #{date_start} to #{date_end}\n"
58
+ start_date_interval = day_interval(pto.start_date_from, pto.start_date_to)
59
+ end_date_interval = day_interval(pto.end_date_from, pto.end_date_to)
60
+
61
+ "#{built_template} from #{start_date_interval} to #{end_date_interval}\n"
60
62
  end
61
63
  end
62
64
 
63
- def same_day_interval(pto)
64
- time_start = format_timezone(pto.start_date).strftime("%I:%M %P")
65
- time_end = format_timezone(pto.end_date).strftime("%I:%M %P")
66
-
67
- time_start == time_end ? "all day" : "from #{time_start} to #{time_end}"
68
- end
65
+ def day_interval(start_date, end_date)
66
+ return start_date.strftime("%F") if end_date.nil?
69
67
 
70
- def format_timezone(date)
71
- date_time = build_date(date)
68
+ time_start = start_date.strftime("%I:%M %P")
69
+ time_end = end_date.strftime("%I:%M %P")
72
70
 
73
- Time.at(date_time, in: @timezone)
71
+ "#{start_date.strftime("%F")} (#{time_start} - #{time_end})"
74
72
  end
75
73
 
76
- def today?(date)
77
- time_now = Time.now.strftime("%F")
74
+ def same_day_interval(pto)
75
+ time_start = pto.start_date_from.strftime("%I:%M %P")
76
+ time_end = pto.end_date_from.strftime("%I:%M %P")
78
77
 
79
- date == format_timezone(time_now).strftime("%F")
78
+ time_start == time_end ? "all day" : "from #{time_start} to #{time_end}"
80
79
  end
81
80
 
82
- def build_date(date)
83
- date_time = date.include?("T") ? date : "#{date}T00:00:00.000#{@timezone}"
84
-
85
- DateTime.parse(date_time).to_time
81
+ def today?(date)
82
+ date == Time.now(in: @timezone).strftime("%F")
86
83
  end
87
84
  end
88
85
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/issue"
4
+ require_relative "../base"
5
+
6
+ module Mapper
7
+ module Github
8
+ ##
9
+ # This class implementats the methods of the Mapper::Base module, specifically designed for
10
+ # preparing or shaping Github issues data coming from a Fetcher::Base implementation.
11
+ class Issues
12
+ include Base
13
+
14
+ # Implements the logic for shaping the results from a fetcher response.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>Fetcher::Github::Types::Response</tt> github_response: Array of github issues data.
19
+ #
20
+ # <br>
21
+ # <b>return</b> <tt>List<Domain::Issue></tt> mapped github issues to be used by a
22
+ # Formatter::Base implementation.
23
+ #
24
+ def map(github_response)
25
+ return [] if github_response.results.empty?
26
+
27
+ normalized_github_data = normalize_response(github_response.results)
28
+
29
+ normalized_github_data.map do |issue|
30
+ Domain::Issue.new(
31
+ issue["title"], issue["state"], issue["assignees"], issue["body"], issue["url"]
32
+ )
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def normalize_response(results)
39
+ return [] if results.nil?
40
+
41
+ results.map do |value|
42
+ {
43
+ "title" => value[:title],
44
+ "state" => value[:state],
45
+ "assignees" => extract_assignees(value[:assignees]),
46
+ "body" => value[:body],
47
+ "url" => value[:url]
48
+ }
49
+ end
50
+ end
51
+
52
+ def extract_assignees(assignees)
53
+ assignees.map { |assignee| assignee[:login] }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -42,19 +42,11 @@ module Mapper
42
42
  response.map do |value|
43
43
  pto_fields = value["properties"].slice(*PTO_PARAMS)
44
44
 
45
- pto_fields.each do |field, pto_value|
46
- pto_fields[field] = extract_pto_value(field, pto_value)
47
- end
48
-
49
- pto_fields
50
- end
51
- end
52
-
53
- def extract_pto_value(field, value)
54
- case field
55
- when "Person" then extract_person_field_value(value)
56
- when "Desde?" then extract_date_field_value(value)
57
- when "Hasta?" then extract_date_field_value(value)
45
+ {
46
+ "Person" => extract_person_field_value(pto_fields["Person"]),
47
+ "Desde?" => extract_date_field_value(pto_fields["Desde?"]),
48
+ "Hasta?" => extract_date_field_value(pto_fields["Hasta?"])
49
+ }
58
50
  end
59
51
  end
60
52
 
@@ -62,9 +54,20 @@ module Mapper
62
54
  data["people"][0]["name"]
63
55
  end
64
56
 
65
- def extract_date_field_value(data)
57
+ def extract_date_field_value(date)
58
+ {
59
+ from: extract_start_date(date),
60
+ to: extract_end_date(date)
61
+ }
62
+ end
63
+
64
+ def extract_start_date(data)
66
65
  data["date"]["start"]
67
66
  end
67
+
68
+ def extract_end_date(data)
69
+ data["date"]["end"]
70
+ end
68
71
  end
69
72
  end
70
73
  end
@@ -27,8 +27,8 @@ module Mapper
27
27
 
28
28
  ptos.map do |pto|
29
29
  name = pto["name"]
30
- start_date = pto["start_date"]
31
- end_date = pto["end_date"]
30
+ start_date = { from: pto["start_date"], to: nil }
31
+ end_date = { from: pto["end_date"], to: nil }
32
32
 
33
33
  Domain::Pto.new(name, start_date, end_date)
34
34
  end
@@ -8,6 +8,7 @@ require_relative "../fetcher/notion/use_case/pto_next_week"
8
8
  require_relative "../fetcher/notion/use_case/work_items_limit"
9
9
  require_relative "../fetcher/postgres/use_case/pto_today"
10
10
  require_relative "../fetcher/imap/use_case/support_emails"
11
+ require_relative "../fetcher/github/use_case/repo_issues"
11
12
 
12
13
  # mapper
13
14
  require_relative "../mapper/notion/birthday_today"
@@ -15,6 +16,7 @@ require_relative "../mapper/notion/pto_today"
15
16
  require_relative "../mapper/notion/work_items_limit"
16
17
  require_relative "../mapper/postgres/pto_today"
17
18
  require_relative "../mapper/imap/support_emails"
19
+ require_relative "../mapper/github/issues"
18
20
 
19
21
  # formatter
20
22
  require_relative "../formatter/birthday"
data/lib/bas/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bas
4
4
  # Gem version
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bas
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kommitters Open Source
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-26 00:00:00.000000000 Z
11
+ date: 2024-04-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A versatile business automation system offering key components for building
14
14
  various use cases. It provides an easy-to-use tool for implementing automation
@@ -41,9 +41,13 @@ files:
41
41
  - lib/bas/domain/birthday.rb
42
42
  - lib/bas/domain/email.rb
43
43
  - lib/bas/domain/exceptions/function_not_implemented.rb
44
+ - lib/bas/domain/issue.rb
44
45
  - lib/bas/domain/pto.rb
45
46
  - lib/bas/domain/work_items_limit.rb
46
47
  - lib/bas/fetcher/base.rb
48
+ - lib/bas/fetcher/github/base.rb
49
+ - lib/bas/fetcher/github/types/response.rb
50
+ - lib/bas/fetcher/github/use_case/repo_issues.rb
47
51
  - lib/bas/fetcher/imap/base.rb
48
52
  - lib/bas/fetcher/imap/types/response.rb
49
53
  - lib/bas/fetcher/imap/use_case/support_emails.rb
@@ -68,6 +72,7 @@ files:
68
72
  - lib/bas/formatter/support_emails.rb
69
73
  - lib/bas/formatter/work_items_limit.rb
70
74
  - lib/bas/mapper/base.rb
75
+ - lib/bas/mapper/github/issues.rb
71
76
  - lib/bas/mapper/imap/support_emails.rb
72
77
  - lib/bas/mapper/notion/birthday_today.rb
73
78
  - lib/bas/mapper/notion/pto_today.rb