asana_snapshot 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +115 -0
- data/bin/snap +9 -0
- data/lib/asana_snapshot/configuration.rb +14 -0
- data/lib/asana_snapshot/persistence/git.rb +23 -0
- data/lib/asana_snapshot/persistence.rb +17 -0
- data/lib/asana_snapshot/snapshot_generator.rb +67 -0
- data/lib/asana_snapshot/task.rb +46 -0
- data/lib/asana_snapshot/task_searcher.rb +35 -0
- data/lib/asana_snapshot.rb +59 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 97597233f2dbc17a8f86f9cfc588a979b308e37b
|
4
|
+
data.tar.gz: 4e0e25b7f25414548ae8bb5f1b74dbcbe2b38bc6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e53bae9059fb06962becb66404d8f895c5e414f8bb873395558fac80206fadb7d06db3175cc996e8ebc957122c54aa1010d3aeafb6bf2d7bfcb71959d57af13
|
7
|
+
data.tar.gz: 88eed4add936e327f4a7cb47448996ebe3bfc095693e91a2ac3b2d7538d0bb528c443f1eae079ba971081ed8cce530c7746f3b7ef23605f04d67266c66e0b1ed
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2018 Matt Yeh
|
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
|
13
|
+
all 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 NONINFRINGEMENT. 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
## Asana Snapshot
|
2
|
+
|
3
|
+
[Asana](https://www.asana.com) is a great, versatile task management system. However, like many similar task management systems, being able to track meaningful progress across one or more projects; burn-down charts are great, but are not built to extract any immediate, actionable decisions. Tracking the progress of a project over a specified period of time can be quite cumbersome, if not impossible. This becomes even more difficult in Asana when using Boards that might all have different names for the swim lanes/columns or that have different terminology for what tasks are 'complete' and 'incomplete'.
|
4
|
+
|
5
|
+
Asana Snapshot has chosen to solve this issue by leveraging a change-tracking tool that is likely a part of most of our daily lives: [git](https://git-scm.com/). Using git comparisons, one can gather information to drive useful & productive conversations about the progress of a given project, such as, overall velocity, project completeness and burndown, which specific tasks were completed in that time period, which specific tasks are at-risk (eg, which ones are assigned, but not completed or which remain unassigned within that given time period), etc.
|
6
|
+
|
7
|
+
From a technical side, this gem will fetch all tasks based on a specific set of user-provided conditions, write to a text-based project snapshot file (ie, using Markdown), commit & tag those project snapshot files into a git repository, while doing so in a simple and repeatable manner.
|
8
|
+
|
9
|
+
### Sample snapshot
|
10
|
+
|
11
|
+
```
|
12
|
+
## Stats
|
13
|
+
Complete: 12
|
14
|
+
Incomplete: 23 (12 unassigned)
|
15
|
+
|
16
|
+
## Tasks
|
17
|
+
[X] 473716394 ["Done"] - Joe Smith - provision test server
|
18
|
+
[ ] 473716485 ["In Progress"] - John Doe - implement authentication service
|
19
|
+
[ ] 473164858 ["Ready To Dev"] - Unassigned - develop profitable feature
|
20
|
+
```
|
21
|
+
|
22
|
+
Tasks are ordered by Asana task id, and so should almost always remain on the same line within a snapshot file, making tasks updates easier to parse out from the git tag comparisons.
|
23
|
+
|
24
|
+
### Installation
|
25
|
+
|
26
|
+
```
|
27
|
+
gem install asana_snapshot
|
28
|
+
```
|
29
|
+
|
30
|
+
or if you are using Bundler add the following to your Gemfile
|
31
|
+
|
32
|
+
```
|
33
|
+
gem 'asana_snapshot'
|
34
|
+
```
|
35
|
+
|
36
|
+
### Configuration
|
37
|
+
|
38
|
+
| Config | Description | Default |
|
39
|
+
| ------------- | --------------------------------------------------------------------- | -------------------:|
|
40
|
+
| logger | gem logging destination | Logger.new STDOUT |
|
41
|
+
| token | Asana API Personal Access Token | nil |
|
42
|
+
| base_dir | file directory where snapshots will be saved | Dir.pwd |
|
43
|
+
| persistence | Hash specifying the persistence store (requires an `adapter` key) | {adapter: :git} |
|
44
|
+
|
45
|
+
Example:
|
46
|
+
```
|
47
|
+
AsanaSnapshot.configure do |config|
|
48
|
+
config.logger = Logger.new('logfile.log')
|
49
|
+
config.token = '_my_personal_token_'
|
50
|
+
config.base_dir = '/path/to/where/i/save/snapshots'
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
### Usage
|
55
|
+
|
56
|
+
There are two primary entry points for executing AsanaSnapshot: via its Ruby API or via the shipped executable file.
|
57
|
+
|
58
|
+
In both cases, it expects the presence of a YAML config file which supplies information about the Asana workspace & projects upon which it will search for tasks. AsanaSnapshot will only search for tasks (as opposed to sub-tasks) that belong to the projects in the YAML file and which contain any tags defined in the YAML file.
|
59
|
+
|
60
|
+
#### Anatomy of a snapshot YAML file
|
61
|
+
|
62
|
+
```
|
63
|
+
title: 'Acme Boards'
|
64
|
+
```
|
65
|
+
The `title` key identifies how the snapshots will be organized. The sub-directory of the configured `base_dir` in which snapshots are saved will be an underscored version of this `title`.
|
66
|
+
|
67
|
+
```
|
68
|
+
workspace: 123456789
|
69
|
+
```
|
70
|
+
The `workspace` key identifies which Asana workspace id to query for tasks.
|
71
|
+
|
72
|
+
```
|
73
|
+
filters:
|
74
|
+
tags: 234567890, 345678901
|
75
|
+
```
|
76
|
+
The `filters` key groups the possible query filters. As of now, only `tags` are supported. `tags` are a comma-delimited list of Asana tag id's that a task may contain.
|
77
|
+
|
78
|
+
```
|
79
|
+
projects:
|
80
|
+
- id: 4567890123
|
81
|
+
name: 'Phase 2 Widgets'
|
82
|
+
columns:
|
83
|
+
complete:
|
84
|
+
- 'Done'
|
85
|
+
incomplete:
|
86
|
+
- 'In Discovery'
|
87
|
+
- 'In Progress'
|
88
|
+
```
|
89
|
+
The `projects` key identifies the set of projects that will be searched. It contains the Asana project id, a helpful `name` to identify it in the YAML file (which would likely match the name of the project in Asana, but it can be anything you want), and a `columns` key which identifies the names of the Asana sections of the project board should be considered as `complete` or `incomplete`.
|
90
|
+
|
91
|
+
#### Ruby API
|
92
|
+
|
93
|
+
Example:
|
94
|
+
```
|
95
|
+
AsanaSnapshot.configure do |config|
|
96
|
+
config.token = '_my_personal_token_'
|
97
|
+
end
|
98
|
+
|
99
|
+
AsanaSnapshot.execute './config/acme_boards.yml'
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Executable
|
103
|
+
|
104
|
+
The gem ships with an executable named `snap`. `snap` takes a single argument, which is the path to the `asana_snapshot` config file.
|
105
|
+
|
106
|
+
The `snap` executable uses the following environment variables to override the default AsanaSnapshot configuration:
|
107
|
+
|
108
|
+
| ENV | Config |
|
109
|
+
| ----------------------- |:-------:|
|
110
|
+
| ASANA_SNAPSHOT_TOKEN | token |
|
111
|
+
|
112
|
+
Example:
|
113
|
+
```
|
114
|
+
ASANA_SNAPSHOT_TOKEN='_my_personal_token_' snap './config/acme_boards.yml'
|
115
|
+
```
|
data/bin/snap
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class AsanaSnapshot::Configuration
|
4
|
+
attr_accessor :logger, :token, :base_dir, :persistence
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@logger = Logger.new STDOUT
|
8
|
+
@token = nil
|
9
|
+
@base_dir = Dir.pwd
|
10
|
+
@persistence = {
|
11
|
+
adapter: :git
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'git'
|
2
|
+
|
3
|
+
class AsanaSnapshot::Persistence::GitAdapter
|
4
|
+
def initialize
|
5
|
+
@repo = Git.init(AsanaSnapshot.configuration.base_dir, log: AsanaSnapshot.configuration.logger)
|
6
|
+
end
|
7
|
+
|
8
|
+
def mark_for_save(file)
|
9
|
+
@repo.add file
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def save(group)
|
14
|
+
today = Time.now.strftime('%Y-%m-%d')
|
15
|
+
@repo.commit "[#{group}] Snapshot: #{today}"
|
16
|
+
|
17
|
+
if @repo.tags.include?(today)
|
18
|
+
@repo.delete_tag today
|
19
|
+
end
|
20
|
+
@repo.add_tag today
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class AsanaSnapshot::Persistence
|
2
|
+
extend Forwardable
|
3
|
+
|
4
|
+
attr_reader :adapter
|
5
|
+
|
6
|
+
def_delegators :adapter, :mark_for_save, :save
|
7
|
+
|
8
|
+
def initialize(adapter: :git)
|
9
|
+
case adapter.to_sym
|
10
|
+
when :git
|
11
|
+
require 'asana_snapshot/persistence/git'
|
12
|
+
@adapter = AsanaSnapshot::Persistence::GitAdapter.new
|
13
|
+
else
|
14
|
+
raise ArgumentError, "Unknown persistence adapter: #{adapter}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module AsanaSnapshot
|
4
|
+
class SnapshotGenerator
|
5
|
+
attr_reader :snapshot_directory, :file, :project_id, :project
|
6
|
+
|
7
|
+
def initialize(tasks, group:, project_id:)
|
8
|
+
@project_id = project_id
|
9
|
+
@project = compile tasks
|
10
|
+
|
11
|
+
directory_name = group.downcase.gsub(' ', '_')
|
12
|
+
@snapshot_directory = "#{AsanaSnapshot.configuration.base_dir}/snapshots/#{directory_name}"
|
13
|
+
|
14
|
+
file_name = project[:name].gsub(' ', '_').gsub('&', 'and')
|
15
|
+
@file = "#{snapshot_directory}/#{file_name}.md"
|
16
|
+
end
|
17
|
+
|
18
|
+
def write
|
19
|
+
setup_directory
|
20
|
+
|
21
|
+
File.open(file, "w") do |f|
|
22
|
+
f.puts '## Stats'
|
23
|
+
f.puts "Complete: #{project[:stats][:complete]}"
|
24
|
+
f.puts "Incomplete: #{project[:stats][:incomplete]} (#{project[:stats][:unassigned]} unassigned)"
|
25
|
+
f.puts ''
|
26
|
+
|
27
|
+
f.puts '## Tasks'
|
28
|
+
project[:tasks].each do |task|
|
29
|
+
f.puts "[#{task.completed? ? 'X' : ' '}] #{task}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
AsanaSnapshot.persistence_store.mark_for_save file
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def setup_directory
|
39
|
+
return if ::File.directory? snapshot_directory
|
40
|
+
::FileUtils.mkdir_p(snapshot_directory)
|
41
|
+
::FileUtils.chmod 0755, snapshot_directory
|
42
|
+
end
|
43
|
+
|
44
|
+
def compile(tasks)
|
45
|
+
tasks.sort_by(&:id).reduce({
|
46
|
+
name: nil,
|
47
|
+
tasks: [],
|
48
|
+
stats: {
|
49
|
+
complete: 0,
|
50
|
+
incomplete: 0,
|
51
|
+
unassigned: 0
|
52
|
+
}
|
53
|
+
}) do |memo, task|
|
54
|
+
task.project_columns.reject do |project_column|
|
55
|
+
project_column.project_id.to_i != project_id.to_i
|
56
|
+
end.each do |project_column|
|
57
|
+
memo[:name] ||= project_column.project_name
|
58
|
+
memo[:tasks].push task
|
59
|
+
memo[:stats][:complete]+=1 if task.completed?
|
60
|
+
memo[:stats][:incomplete]+=1 unless task.completed?
|
61
|
+
memo[:stats][:unassigned]+=1 if task.assignee_name.nil?
|
62
|
+
end
|
63
|
+
memo
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module AsanaSnapshot
|
2
|
+
class Task
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@task, :id, :name
|
6
|
+
|
7
|
+
def initialize(task_response)
|
8
|
+
@task = OpenStruct.new(task_response)
|
9
|
+
end
|
10
|
+
|
11
|
+
def assignee_name
|
12
|
+
@task.assignee&.send(:[], 'name')
|
13
|
+
end
|
14
|
+
|
15
|
+
def completed?
|
16
|
+
completed_task? || completed_column?
|
17
|
+
end
|
18
|
+
|
19
|
+
def project_columns
|
20
|
+
@project_columns ||= @task.memberships.map do |m|
|
21
|
+
OpenStruct.new(
|
22
|
+
project_id: m['project']['id'],
|
23
|
+
project_name: m['project']['name'],
|
24
|
+
column_name: m['section']['name']
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#{id} #{project_columns.map(&:column_name)} - #{assignee_name || 'Unassigned'} - #{name}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def completed_task?
|
34
|
+
@task.completed
|
35
|
+
end
|
36
|
+
|
37
|
+
def completed_column?
|
38
|
+
project_columns.map do |project_column|
|
39
|
+
project_config = AsanaSnapshot.projects.detect do |project|
|
40
|
+
project['id'] == project_column.project_id
|
41
|
+
end
|
42
|
+
project_config && project_config['columns']['complete']&.include?(project_column.column_name)
|
43
|
+
end.any?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'asana'
|
2
|
+
require_relative './task'
|
3
|
+
|
4
|
+
module AsanaSnapshot
|
5
|
+
class TaskSearcher
|
6
|
+
include Asana::Resources::ResponseHelper
|
7
|
+
extend Asana::Resources::ResponseHelper
|
8
|
+
|
9
|
+
attr_reader :client, :workspace_id
|
10
|
+
|
11
|
+
TASK_FIELDS = {
|
12
|
+
fields: [
|
13
|
+
'completed',
|
14
|
+
'name',
|
15
|
+
'assignee.name',
|
16
|
+
'memberships.(project|section).name'
|
17
|
+
]
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def initialize(token: required('token'), workspace_id: required('workspace_id'))
|
21
|
+
@client = Asana::Client.new do |c|
|
22
|
+
c.authentication :access_token, token
|
23
|
+
end
|
24
|
+
@workspace_id = workspace_id
|
25
|
+
end
|
26
|
+
|
27
|
+
def search(search_options = {})
|
28
|
+
endpoint = "/workspaces/#{workspace_id}/tasks/search"
|
29
|
+
search_results = parse client.get(endpoint, params: search_options, options: TASK_FIELDS)
|
30
|
+
search_results.first.map do |search_result|
|
31
|
+
AsanaSnapshot::Task.new search_result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
require 'asana_snapshot/task_searcher'
|
5
|
+
require 'asana_snapshot/snapshot_generator'
|
6
|
+
require 'asana_snapshot/configuration'
|
7
|
+
require 'asana_snapshot/persistence'
|
8
|
+
|
9
|
+
module AsanaSnapshot
|
10
|
+
class << self
|
11
|
+
attr_accessor :projects
|
12
|
+
attr_writer :configuration, :persistence_store
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configuration
|
16
|
+
@configuration ||= AsanaSnapshot::Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.persistence_store
|
20
|
+
@persistence_store ||= AsanaSnapshot::Persistence.new(adapter: AsanaSnapshot.configuration.persistence[:adapter])
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.configure
|
24
|
+
yield configuration
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.execute(config_file)
|
28
|
+
unless self.configuration.token
|
29
|
+
self.configuration.logger.error "No Asana token configured."
|
30
|
+
else
|
31
|
+
config = YAML.load_file config_file
|
32
|
+
|
33
|
+
self.projects = config['projects']
|
34
|
+
self.projects.each do |project|
|
35
|
+
tasks = AsanaSnapshot::TaskSearcher.new(
|
36
|
+
token: self.configuration.token,
|
37
|
+
workspace_id: config['workspace']
|
38
|
+
).search(
|
39
|
+
'tags.any' => config['filters']['tags'],
|
40
|
+
'projects.any' => project['id'],
|
41
|
+
'is_subtask' => false
|
42
|
+
)
|
43
|
+
|
44
|
+
if tasks.any?
|
45
|
+
AsanaSnapshot::SnapshotGenerator.new(
|
46
|
+
tasks,
|
47
|
+
group: config['title'],
|
48
|
+
project_id: project['id']
|
49
|
+
).write
|
50
|
+
self.configuration.logger.info "Successfully created snapshot for #{project['name']}"
|
51
|
+
else
|
52
|
+
self.configuration.logger.info "No tasks found for #{project['name']}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
self.persistence_store.save config['title']
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: asana_snapshot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Yeh
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: asana
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.8.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.8'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: git
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.5.0
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.5.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 1.5.0
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.5.0
|
53
|
+
description: " AsanaSnapshot wraps the official Asana ruby client to search for
|
54
|
+
tasks, write them to text files, and check them into a git repository.\n"
|
55
|
+
email: dev.mtyeh411@gmail.com
|
56
|
+
executables:
|
57
|
+
- snap
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- bin/snap
|
64
|
+
- lib/asana_snapshot.rb
|
65
|
+
- lib/asana_snapshot/configuration.rb
|
66
|
+
- lib/asana_snapshot/persistence.rb
|
67
|
+
- lib/asana_snapshot/persistence/git.rb
|
68
|
+
- lib/asana_snapshot/snapshot_generator.rb
|
69
|
+
- lib/asana_snapshot/task.rb
|
70
|
+
- lib/asana_snapshot/task_searcher.rb
|
71
|
+
homepage: https://github.com/mtyeh411/asana_snapshot
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 2.4.2
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 2.6.14
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Save snapshots of Asana tasks.
|
95
|
+
test_files: []
|