torque 0.0.1

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.
@@ -0,0 +1,166 @@
1
+ require_relative 'file_system'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Determines the date range for a release notes generation
7
+ class DateSettings
8
+
9
+ ##
10
+ # @param options A hash of the command line options used to run Torque
11
+ # @param last_run_path The path to the lastRun file
12
+ # @param fs An instance of the FileSystem class
13
+ def initialize(options, last_run_path, fs)
14
+ @options = options
15
+ @last_run_path = last_run_path
16
+ @fs = fs
17
+
18
+ @current_date = Date.today
19
+ @beginning_of_time = Date.new(1900, 1, 1)
20
+ @date_format = "%Y-%m-%d"
21
+ end
22
+
23
+ ##
24
+ # Generates the accept_from and accept_to dates if they don't already exist
25
+ #
26
+ # Returns [accept_from, accept_to]
27
+ def get_dates
28
+ generate_dates if !@accept_from || !@accept_to
29
+ return @accept_from, @accept_to
30
+ end
31
+
32
+ ##
33
+ # Returns true if the date range was set manually via command line, else false
34
+ def custom_date_range?
35
+ generate_dates if !@custom_date_range
36
+ return @custom_date_range
37
+ end
38
+
39
+
40
+ private
41
+
42
+ # Generates values for @accept_from and @accept_to
43
+ # Returns [@accept_from, @accept_to]
44
+ def generate_dates
45
+
46
+ # If custom date settings exist, takes dates from there
47
+ if @options[:accept_from] || @options[:accept_to]
48
+ get_dates_from_custom_range
49
+ @custom_date_range = true
50
+
51
+ # If not:
52
+ # @accept_to = today
53
+ # @accept_from is calculated from the dates of previous generations
54
+ else
55
+ get_dates_from_default
56
+ @custom_date_range = false
57
+ end
58
+
59
+ return @accept_from, @accept_to
60
+
61
+ end
62
+
63
+ # Handles date values that were set manually in the environment
64
+ # Returns [@accept_from, @accept_to]
65
+ def get_dates_from_custom_range
66
+ begin
67
+ @accept_from = Date.parse(@options[:accept_from].nil? ? "1900-01-01" : @options[:accept_from])
68
+ rescue ArgumentError
69
+ raise ArgumentError.new("Date #{@options[:accept_from]} could not be parsed")
70
+ end
71
+
72
+ begin
73
+ @accept_to = (@options[:accept_to].nil? ? @current_date : Date.parse(@options[:accept_to]))
74
+ rescue ArgumentError
75
+ raise ArgumentError.new("Date #{@options[:accept_to]} could not be parsed")
76
+ end
77
+
78
+ if @accept_from > @accept_to
79
+ raise ArgumentError.new("Date ACCEPT_FROM (#{@accept_from}) occurs after ACCEPT_TO (#{@accept_to})")
80
+ end
81
+
82
+ return @accept_from, @accept_to
83
+ end
84
+
85
+ # dates: An array of the dates contained by .lastRun
86
+ #
87
+ # Reads from the .last-run file for the project to figure out when notes were last generated
88
+ # Returns [@accept_from, @accept_to]
89
+ def get_dates_from_default
90
+
91
+ dates = update_last_run_file
92
+
93
+ @accept_to = @current_date
94
+ @accept_from = dates[1] || @beginning_of_time
95
+
96
+ return @accept_from, @accept_to
97
+ end
98
+
99
+ # Updates .last-run on the filesystem, returning the dates it contains after the update
100
+ def update_last_run_file
101
+
102
+ # Structure of last-run file:
103
+ #
104
+ # <Most recent generation date>
105
+ # <Second most recent generation date>
106
+
107
+ # Creates the .last-run if none exists
108
+ if !@fs.path_exist? @last_run_path
109
+ @fs.file_create(@last_run_path)
110
+ end
111
+
112
+ # Generates and writes the new file contents
113
+ new_file_str, dates = update_last_run_file_string(@fs.file_read(@last_run_path))
114
+ @fs.file_write(@last_run_path, new_file_str)
115
+
116
+ # Returns the new dates
117
+ dates
118
+
119
+ end
120
+
121
+ # Takes in a string with the contents of the last-run file, outputs a string with its new contents
122
+ def update_last_run_file_string(file_str)
123
+
124
+ # Parses the dates in .last-run
125
+ date_strings = []
126
+ file_str.each_line do |date_str|
127
+ date_strings << date_str
128
+ end
129
+ dates = date_strings.map do |date_str|
130
+ Date.strptime(date_str, @date_format)
131
+ end
132
+
133
+ #Generates new dates
134
+ new_dates = []
135
+
136
+ if dates.length == 0
137
+ new_dates = [@current_date]
138
+
139
+ elsif dates.length == 1
140
+ if dates[0] == @current_date
141
+ new_dates = [dates[0]]
142
+ else
143
+ new_dates = [@current_date, dates[0]]
144
+ end
145
+
146
+ else
147
+ if dates[0] == @current_date
148
+ new_dates = [dates[0], dates[1]]
149
+ else
150
+ new_dates = [@current_date, dates[0]]
151
+ end
152
+ end
153
+
154
+ # Generates a new file string
155
+ new_file_str = ""
156
+ new_dates.each do
157
+ |date|
158
+ new_file_str += "#{date.strftime(@date_format)}\n"
159
+ end
160
+
161
+ # Returns [new_file_str, new_dates]
162
+ return new_file_str, new_dates
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'pivotal_api_error'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Error to be thrown if a Pivotal Tracker project ID is invalid
7
+ class InvalidProjectError < PivotalAPIError
8
+ end
9
+
10
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'pivotal_api_error'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Error to be thrown if a Pivotal Tracker API token is invalid
7
+ class InvalidTokenError < PivotalAPIError
8
+ end
9
+
10
+ end
@@ -0,0 +1,7 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to throw if the output directory is missing
5
+ class MissingOutputDirectoryError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if a Pivotal Tracker project ID cannot be found
5
+ class MissingProjectError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if a Pivotal Tracker API token cannot be found
5
+ class MissingTokenError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Error to be thrown if no .torqueinfo.yaml file can be found
5
+ class MissingTorqueInfoFileError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Torque
2
+
3
+ ##
4
+ # Catch-all class for errors when making requests to the Pivotal Tracker API
5
+ class PivotalAPIError < StandardError
6
+ end
7
+
8
+ end
@@ -0,0 +1,70 @@
1
+ require 'fileutils'
2
+ require 'pathname'
3
+
4
+ class Torque
5
+
6
+ ##
7
+ # Creates a (limited) interface through which Torque can interact with the local file system
8
+ #
9
+ # Supports:
10
+ #
11
+ # * File creation, reading, line-by-line iteration, and overwriting
12
+ #
13
+ # * Directory creation
14
+ #
15
+ # * Pathname checking
16
+ #
17
+ class FileSystem
18
+
19
+ def initialize
20
+ # Do nothing. The file system's properties are automatically global
21
+ end
22
+
23
+ ##
24
+ # Creates a file, optionally overwriting an existing file to do so
25
+ def file_create(filename, over=false)
26
+ if !path_exist? filename || over
27
+ File.open(filename, "w")
28
+ else
29
+ raise IOError.new "File #{filename} already exists"
30
+ end
31
+ end
32
+
33
+ ##
34
+ # Returns an iterator over each line of a file
35
+ def file_each_line(filename)
36
+ File.open(filename, "r").each_line
37
+ end
38
+
39
+ ##
40
+ # Returns the contents of a file
41
+ def file_read(filename)
42
+ File.read(filename)
43
+ end
44
+
45
+ ##
46
+ # Writes a string to file
47
+ def file_write(filename, string)
48
+ File.write(filename, string)
49
+ end
50
+
51
+ ##
52
+ # Creates a directory
53
+ def mkdir(dirname)
54
+ FileUtils.mkdir(dirname)
55
+ end
56
+
57
+ ##
58
+ # Creates a directory, creating intermediate directories as needed
59
+ def mkdir_p(dirname)
60
+ FileUtils.mkdir_p(dirname)
61
+ end
62
+
63
+ ##
64
+ # Returns true if a pathname exists, else false
65
+ def path_exist?(pathname)
66
+ Pathname.new(pathname).exist?
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,62 @@
1
+ require 'mail'
2
+
3
+ class Torque
4
+
5
+ ##
6
+ # Handles sending the finished release notes document to a list of emails after Torque generates it
7
+ class Mailer
8
+
9
+ ##
10
+ # @param email An email address
11
+ # @param password A password that matches the email address given
12
+ #
13
+ # Creates a Mailer that will send emails from the email address/password combo given
14
+ def initialize(email, password)
15
+ @email = email
16
+ @password = password
17
+
18
+ options = { :address => "smtp.gmail.com",
19
+ :port => 587,
20
+ :domain => 'gmail.com',
21
+ :user_name => @email,
22
+ :password => @password,
23
+ :authentication => 'plain',
24
+ :enable_starttls_auto => true }
25
+
26
+ Mail.defaults do
27
+ delivery_method :smtp, options
28
+ end
29
+ end
30
+
31
+ # ##
32
+ # # Returns true if the email and password combination is valid, else false
33
+ # def verify
34
+ # end
35
+
36
+ # @param notes_string The release notes file in string form
37
+ # @param subject_line The subject line to use in the email
38
+ # @param address_list A comma-separated list of email addresses to which to send the notes
39
+ #
40
+ # Sends notes_string as an email with subject_line from an arbitrary email address to everyone on address_list
41
+ def send_notes(notes_string, subject_line, address_list)
42
+
43
+ mail = Mail::Message.new
44
+
45
+ mail.charset = "UTF-8"
46
+ mail.to address_list
47
+ mail.from @email
48
+ mail.subject subject_line
49
+ mail.body notes_string
50
+
51
+ begin
52
+ mail.deliver
53
+ rescue Net::SMTPAuthenticationError
54
+ # TODO Remove this output. Should fail silently and be replaced by an independent call to :verify
55
+ puts "Username and password not accepted by Gmail. Check your username and password or try using a Gmail " \
56
+ "account"
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,98 @@
1
+ require 'net/http'
2
+ require_relative 'error/invalid_project_error'
3
+ require_relative 'error/invalid_token_error'
4
+ require_relative 'error/pivotal_api_error'
5
+
6
+ class Torque
7
+ class Pivotal
8
+
9
+ ##
10
+ # @param token A Pivotal Tracker api token
11
+ def initialize(token)
12
+ @token = token
13
+ end
14
+
15
+ ##
16
+ # Returns true if a connection to www.pivotaltracker.com exists, else false
17
+ def self.connection?
18
+ begin
19
+ TCPSocket.new "www.pivotaltracker.com", 80
20
+ true
21
+ rescue SocketError
22
+ false
23
+ end
24
+ end
25
+
26
+ ##
27
+ # Returns true if the token supplied is a valid token, else false
28
+ def check_token
29
+ begin
30
+ get_project_data
31
+ true
32
+ rescue InvalidTokenError
33
+ false
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Sends a request through the Pivotal Tracker API
39
+ #
40
+ # Returns a string of html data from Pivotal Tracker with data on the stories for the given project
41
+ def get_project_stories(project)
42
+
43
+ # Polls story data from pivotal tracker
44
+ host="pivotaltracker.com"
45
+ port=80
46
+ url="http://www.pivotaltracker.com/services/v3/projects/#{project}/stories"
47
+ headers={'X-TrackerToken'=>@token}
48
+
49
+ response=Net::HTTP.new(host, port).get(url, headers)
50
+
51
+ # Handles network errors
52
+ if response.code == "401"
53
+ raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid for project #{project}"
54
+
55
+ elsif response.code == "500"
56
+ raise InvalidProjectError.new "The Pivotal Tracker project ID supplied, #{project}, is invalid"
57
+
58
+ elsif response.code != "200"
59
+ raise PivotalAPIError.new(
60
+ "The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
61
+ + "project ID, API token, and/or internet connection"
62
+ )
63
+
64
+ end
65
+
66
+ response.body
67
+ end
68
+
69
+ ##
70
+ # Sends a request through the Pivotal Tracker API
71
+ #
72
+ # Returns a string of html data from Pivotal Tracker with data on each of a user's projects
73
+ def get_project_data
74
+
75
+ host="pivotaltracker.com"
76
+ port=80
77
+ url="http://www.pivotaltracker.com/services/v3/projects"
78
+ headers={'X-TrackerToken'=>@token}
79
+
80
+ response=Net::HTTP.new(host, port).get(url, headers)
81
+
82
+ # Handles network errors
83
+ if response.code == "401"
84
+ raise InvalidTokenError.new "The Pivotal Tracker API token supplied is not valid"
85
+
86
+ elsif response.code != "200"
87
+ raise PivotalAPIError.new(
88
+ "The Pivotal Tracker API responded with an unexpected error code: #{response.code}. Check your " \
89
+ "API token, and/or internet connection"
90
+ )
91
+ end
92
+
93
+ response.body
94
+ end
95
+
96
+
97
+ end
98
+ end