duolingo_personal_data 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 +4 -4
- data/.rubocop.yml +5 -5
- data/.rubocop_todo.yml +1 -44
- data/CHANGELOG.md +14 -0
- data/Gemfile +4 -4
- data/Rakefile +10 -8
- data/duolingo_personal_data.gemspec +14 -13
- data/lib/duolingo_personal_data/auth_data.rb +7 -7
- data/lib/duolingo_personal_data/avatar_images.rb +5 -5
- data/lib/duolingo_personal_data/blast_emails.rb +17 -17
- data/lib/duolingo_personal_data/directory.rb +55 -0
- data/lib/duolingo_personal_data/friends_follow.rb +5 -5
- data/lib/duolingo_personal_data/inventory.rb +10 -10
- data/lib/duolingo_personal_data/languages.rb +13 -13
- data/lib/duolingo_personal_data/leaderboards.rb +5 -5
- data/lib/duolingo_personal_data/notify_data.rb +4 -4
- data/lib/duolingo_personal_data/profile.rb +13 -13
- data/lib/duolingo_personal_data/stories.rb +3 -3
- data/lib/duolingo_personal_data/story_completions.rb +4 -4
- data/lib/duolingo_personal_data/teacher_privacy_settings.rb +8 -8
- data/lib/duolingo_personal_data/version.rb +1 -1
- data/lib/duolingo_personal_data.rb +14 -13
- data/manifest.scm +1 -1
- data/sig/duolingo_personal_data.rbs +38 -8
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1b983dd563cc933d3a549d692b2e454cba4fd0413132d93761bc874a425af91
|
|
4
|
+
data.tar.gz: '0497cd092c7b84103da7c1b3f9a4293386cbf157f164d8c8a5d5f9063b6daebf'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b465b8f05db11c503afa124ac232e1075bf2a7485449a5066b045b11ca77a7755000e340543f62046f82f0ffbef7c3d6872682e35e594f680e352de26ff3ff8e
|
|
7
|
+
data.tar.gz: 981c6cacd459875817f8e3cd10dc86c9ea209afccd83c05e2a231b41dd51e9ae1143c609811f5c7948de1968c6911896357ce878630ccb6efe73451c3140fae4
|
data/.rubocop.yml
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
|
2
2
|
|
|
3
|
+
require:
|
|
4
|
+
- rubocop-rake
|
|
5
|
+
|
|
3
6
|
AllCops:
|
|
4
7
|
TargetRubyVersion: 2.6
|
|
5
8
|
NewCops: enable
|
|
9
|
+
SuggestExtensions: false # RuboCop suggests extensions even if it's installed
|
|
10
|
+
DisabledByDefault: true
|
|
6
11
|
|
|
7
12
|
Style/StringLiterals:
|
|
8
13
|
Enabled: true
|
|
9
|
-
EnforcedStyle: double_quotes
|
|
10
14
|
|
|
11
15
|
Style/StringLiteralsInInterpolation:
|
|
12
16
|
Enabled: true
|
|
13
|
-
EnforcedStyle: double_quotes
|
|
14
|
-
|
|
15
|
-
Layout/LineLength:
|
|
16
|
-
Enabled: false
|
|
17
17
|
|
|
18
18
|
Style/FrozenStringLiteralComment:
|
|
19
19
|
Enabled: true
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,50 +1,7 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2023-
|
|
3
|
+
# on 2023-06-18 09:30:04 UTC using RuboCop version 1.48.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
|
8
|
-
|
|
9
|
-
# Offense count: 2
|
|
10
|
-
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
|
|
11
|
-
Metrics/AbcSize:
|
|
12
|
-
Max: 34
|
|
13
|
-
|
|
14
|
-
# Offense count: 1
|
|
15
|
-
# Configuration parameters: CountComments, CountAsOne.
|
|
16
|
-
Metrics/ClassLength:
|
|
17
|
-
Max: 114
|
|
18
|
-
|
|
19
|
-
# Offense count: 1
|
|
20
|
-
# Configuration parameters: IgnoredMethods.
|
|
21
|
-
Metrics/CyclomaticComplexity:
|
|
22
|
-
Max: 11
|
|
23
|
-
|
|
24
|
-
# Offense count: 2
|
|
25
|
-
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
|
|
26
|
-
Metrics/MethodLength:
|
|
27
|
-
Max: 18
|
|
28
|
-
|
|
29
|
-
# Offense count: 1
|
|
30
|
-
# Configuration parameters: IgnoredMethods.
|
|
31
|
-
Metrics/PerceivedComplexity:
|
|
32
|
-
Max: 11
|
|
33
|
-
|
|
34
|
-
# Offense count: 12
|
|
35
|
-
Style/Documentation:
|
|
36
|
-
Exclude:
|
|
37
|
-
- 'spec/**/*'
|
|
38
|
-
- 'test/**/*'
|
|
39
|
-
- 'lib/duolingo_personal_data/auth_data.rb'
|
|
40
|
-
- 'lib/duolingo_personal_data/avatar_images.rb'
|
|
41
|
-
- 'lib/duolingo_personal_data/blast_emails.rb'
|
|
42
|
-
- 'lib/duolingo_personal_data/friends_follow.rb'
|
|
43
|
-
- 'lib/duolingo_personal_data/inventory.rb'
|
|
44
|
-
- 'lib/duolingo_personal_data/languages.rb'
|
|
45
|
-
- 'lib/duolingo_personal_data/leaderboards.rb'
|
|
46
|
-
- 'lib/duolingo_personal_data/notify_data.rb'
|
|
47
|
-
- 'lib/duolingo_personal_data/profile.rb'
|
|
48
|
-
- 'lib/duolingo_personal_data/stories.rb'
|
|
49
|
-
- 'lib/duolingo_personal_data/story_completions.rb'
|
|
50
|
-
- 'lib/duolingo_personal_data/teacher_privacy_settings.rb'
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* Direcotry class (`DuolingoPersonalData::Direcotry`)
|
|
8
|
+
|
|
9
|
+
### Changed, Removed, Fixed
|
|
10
|
+
|
|
11
|
+
* Changed to not delegate most Array and Hash methods
|
|
12
|
+
* Update RBS
|
|
13
|
+
|
|
14
|
+
### Others
|
|
15
|
+
|
|
16
|
+
* Set RuboCop config to disable by default
|
|
17
|
+
* Lint with RuboCop
|
|
18
|
+
|
|
5
19
|
## [0.1.0] - 2023-03-19
|
|
6
20
|
|
|
7
21
|
Initial release.
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rake/testtask'
|
|
3
3
|
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
|
5
|
-
t.libs <<
|
|
6
|
-
t.libs <<
|
|
7
|
-
t.test_files = FileList[
|
|
5
|
+
t.libs << 'test'
|
|
6
|
+
t.libs << 'lib'
|
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
require
|
|
10
|
+
require 'rubocop/rake_task'
|
|
11
11
|
|
|
12
12
|
RuboCop::RakeTask.new
|
|
13
13
|
|
|
14
14
|
task default: %i[test rubocop]
|
|
15
15
|
|
|
16
|
+
desc 'serve generated API documentation'
|
|
16
17
|
task :serve do
|
|
17
|
-
sh
|
|
18
|
+
sh 'ruby -run -e httpd doc'
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
desc 'generate type signatures'
|
|
20
22
|
task :sig do
|
|
21
|
-
sh
|
|
23
|
+
sh 'typeprof lib/**/* > sig/duolingo_personal_data.rbs'
|
|
22
24
|
end
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
require_relative
|
|
1
|
+
require_relative 'lib/duolingo_personal_data/version'
|
|
2
2
|
|
|
3
3
|
Gem::Specification.new do |spec|
|
|
4
|
-
spec.name =
|
|
4
|
+
spec.name = 'duolingo_personal_data'
|
|
5
5
|
spec.version = DuolingoPersonalData::VERSION
|
|
6
|
-
spec.authors = [
|
|
7
|
-
spec.email = [
|
|
6
|
+
spec.authors = ['gemmaro']
|
|
7
|
+
spec.email = ['gemmaro.dev@gmail.com']
|
|
8
8
|
|
|
9
|
-
spec.summary =
|
|
10
|
-
spec.description =
|
|
11
|
-
spec.homepage =
|
|
12
|
-
spec.license =
|
|
13
|
-
spec.required_ruby_version =
|
|
9
|
+
spec.summary = 'Library for Duolingo personal data'
|
|
10
|
+
spec.description = 'Duolingo Personal Data gem is for loading Duolingo Personal Data, which can be acquired at https://drive-thru.duolingo.com/.'
|
|
11
|
+
spec.homepage = 'https://gitlab.com/gemmaro/ruby-duolingo-personal-data'
|
|
12
|
+
spec.license = 'Apache-2.0'
|
|
13
|
+
spec.required_ruby_version = '>= 2.6.0'
|
|
14
14
|
|
|
15
|
-
spec.metadata[
|
|
16
|
-
spec.metadata[
|
|
17
|
-
spec.metadata[
|
|
15
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
16
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
17
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/-/blob/main/CHANGELOG.md"
|
|
18
18
|
|
|
19
19
|
spec.files = Dir.chdir(__dir__) do
|
|
20
20
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
@@ -22,5 +22,6 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
spec.require_paths = [
|
|
25
|
+
spec.require_paths = ['lib']
|
|
26
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
27
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'csv'
|
|
2
2
|
|
|
3
3
|
module DuolingoPersonalData
|
|
4
4
|
class AuthData
|
|
@@ -7,29 +7,29 @@ module DuolingoPersonalData
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def user_account_name
|
|
10
|
-
@user_account_name ||= value_from_property(
|
|
10
|
+
@user_account_name ||= value_from_property('User Account Name')
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def email_address
|
|
14
|
-
@email_address ||= value_from_property(
|
|
14
|
+
@email_address ||= value_from_property('Email Address')
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def last_update_timestamp
|
|
18
|
-
@last_update_timestamp ||= value_from_property(
|
|
18
|
+
@last_update_timestamp ||= value_from_property('Last Update Timestamp')
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def last_login_attempt_timestamp
|
|
22
|
-
@last_login_attempt_timestamp ||= value_from_property(
|
|
22
|
+
@last_login_attempt_timestamp ||= value_from_property('Last Login Attempt Timestamp')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def last_authentication_key_refresh_timestamp
|
|
26
|
-
@last_authentication_key_refresh_timestamp ||= value_from_property(
|
|
26
|
+
@last_authentication_key_refresh_timestamp ||= value_from_property('Last Authentication Key Refresh Timestamp')
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
def value_from_property(property_name)
|
|
32
|
-
table.find { |row| row[
|
|
32
|
+
table.find { |row| row['Property'] == property_name }['Value']
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def table
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
3
|
-
require
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'forwardable'
|
|
4
4
|
|
|
5
5
|
module DuolingoPersonalData
|
|
6
6
|
class AvatarImages
|
|
@@ -9,12 +9,12 @@ module DuolingoPersonalData
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
extend Forwardable
|
|
12
|
-
def_delegators :urls,
|
|
12
|
+
def_delegators :urls, :to_a, :first, :size # TODO: Add more as needed
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
16
|
def urls
|
|
17
|
-
@urls ||= table.map { |row| URI(row[
|
|
17
|
+
@urls ||= table.map { |row| URI(row['URLs']) }
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def table
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'time'
|
|
3
3
|
|
|
4
4
|
module DuolingoPersonalData
|
|
5
5
|
class BlastEmails
|
|
@@ -8,60 +8,60 @@ module DuolingoPersonalData
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def email_address
|
|
11
|
-
@email_address ||= value_from_property(
|
|
11
|
+
@email_address ||= value_from_property('email_address')
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def ui_language
|
|
15
|
-
@ui_language ||= value_from_property(
|
|
15
|
+
@ui_language ||= value_from_property('ui_language')
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def learning_language
|
|
19
|
-
@learning_language ||= value_from_property(
|
|
19
|
+
@learning_language ||= value_from_property('learning_language')
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def enabled
|
|
23
|
-
@enabled ||= boolean_value_from_property(
|
|
23
|
+
@enabled ||= boolean_value_from_property('enabled')
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def announcement
|
|
27
|
-
@announcement ||= boolean_value_from_property(
|
|
27
|
+
@announcement ||= boolean_value_from_property('announcement')
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def creation_datetime
|
|
31
|
-
@creation_datetime ||= time_value_from_property(
|
|
31
|
+
@creation_datetime ||= time_value_from_property('creation_datetime')
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def last_session
|
|
35
|
-
@last_session ||= time_value_from_property(
|
|
35
|
+
@last_session ||= time_value_from_property('last_session')
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def trial_user
|
|
39
|
-
@trial_user ||= boolean_value_from_property(
|
|
39
|
+
@trial_user ||= boolean_value_from_property('trial_user')
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def country
|
|
43
|
-
@country ||= value_from_property(
|
|
43
|
+
@country ||= value_from_property('country')
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def client
|
|
47
|
-
@client ||= value_from_property(
|
|
47
|
+
@client ||= value_from_property('client')
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def schools_role
|
|
51
|
-
@schools_role ||= integer_value_from_property(
|
|
51
|
+
@schools_role ||= integer_value_from_property('schools_role')
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
private
|
|
55
55
|
|
|
56
56
|
def value_from_property(property_name)
|
|
57
|
-
table.find { |row| row[
|
|
57
|
+
table.find { |row| row['property'] == property_name }['value']
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def boolean_value_from_property(property_name)
|
|
61
61
|
value = value_from_property(property_name)
|
|
62
62
|
case value
|
|
63
|
-
when
|
|
64
|
-
when
|
|
63
|
+
when '0' then false
|
|
64
|
+
when '1' then true
|
|
65
65
|
else
|
|
66
66
|
raise Error, "cannot interpret #{value.inspect} as boolean"
|
|
67
67
|
end
|
|
@@ -69,7 +69,7 @@ module DuolingoPersonalData
|
|
|
69
69
|
|
|
70
70
|
def time_value_from_property(property_name)
|
|
71
71
|
value = value_from_property(property_name)
|
|
72
|
-
Time.strptime(value,
|
|
72
|
+
Time.strptime(value, '%Y-%m-%d %T')
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def integer_value_from_property(property_name)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module DuolingoPersonalData
|
|
2
|
+
class Directory
|
|
3
|
+
def initialize(path)
|
|
4
|
+
@path = path
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def auth_data
|
|
8
|
+
@auth_data ||= AuthData.new(File.join(@path, 'auth_data.csv'))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def avatar_images
|
|
12
|
+
@avatar_images ||= AvatarImages.new(File.join(@path, 'avatar_images.csv'))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blast_emails
|
|
16
|
+
@blast_emails ||= BlastEmails.new(File.join(@path, 'duolingo-blast-emails.csv'))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def notify_data
|
|
20
|
+
@notify_data ||= NotifyData.new(File.join(@path, 'duolingo-notify-data.csv'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def friends_follow
|
|
24
|
+
@friends_follow ||= FriendsFollow.new(File.join(@path, 'friends-follow.csv'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def inventory
|
|
28
|
+
@inventory ||= Inventory.new(File.join(@path, 'inventory.csv'))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def languages
|
|
32
|
+
@languages ||= Languages.new(File.join(@path, 'languages.csv'))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def leaderboards
|
|
36
|
+
@leaderboards ||= Leaderboards.new(File.join(@path, 'leaderboards.csv'))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def profile
|
|
40
|
+
@profile ||= Profile.new(File.join(@path, 'profile.csv'))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stories
|
|
44
|
+
@stories ||= Stories.new(File.join(@path, 'stories.csv'))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def story_completions
|
|
48
|
+
@story_completions ||= StoryCompletions.new(File.join(@path, 'stories-story-completions.csv'))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def teacher_privacy_settings
|
|
52
|
+
@teacher_privacy_settings ||= TeacherPrivacySettings.new(File.join(@path, 'TeacherPrivacySettings.csv'))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -5,23 +5,23 @@ module DuolingoPersonalData
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def num_following
|
|
8
|
-
@num_following ||= integer_value_from_property(
|
|
8
|
+
@num_following ||= integer_value_from_property('num_following')
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def num_followers
|
|
12
|
-
@num_followers ||= integer_value_from_property(
|
|
12
|
+
@num_followers ||= integer_value_from_property('num_followers')
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def num_blocking
|
|
16
|
-
@num_blocking ||= integer_value_from_property(
|
|
16
|
+
@num_blocking ||= integer_value_from_property('num_blocking')
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def num_blockers
|
|
20
|
-
@num_blockers ||= integer_value_from_property(
|
|
20
|
+
@num_blockers ||= integer_value_from_property('num_blockers')
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def timestamp_generated
|
|
24
|
-
@timestamp_generated ||= timestamp_value_from_property(
|
|
24
|
+
@timestamp_generated ||= timestamp_value_from_property('timestamp_generated')
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'forwardable'
|
|
2
2
|
|
|
3
3
|
module DuolingoPersonalData
|
|
4
4
|
InventoryItem = Struct.new(:item_type, :purchase_datetime, :active, :price_in_virtual_currency, :wager_day,
|
|
@@ -10,32 +10,32 @@ module DuolingoPersonalData
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
extend Forwardable
|
|
13
|
-
def_delegators :items,
|
|
13
|
+
def_delegators :items, :to_a, :first, :size, :[] # TODO: Add more as needed
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def items
|
|
18
18
|
@items ||= table.map do |row|
|
|
19
|
-
item = InventoryItem.new(item_type: row[
|
|
20
|
-
purchase_datetime: parse_datetime(row[
|
|
21
|
-
price = row[
|
|
19
|
+
item = InventoryItem.new(item_type: row['item_type'],
|
|
20
|
+
purchase_datetime: parse_datetime(row['purchase_datetime']), active: parse_boolean(row['active']), payment_processor: row['payment_processor'], product: row['product'])
|
|
21
|
+
price = row['price_in_virtual_currency']
|
|
22
22
|
item.price_in_virtual_currency = Integer(price) if price
|
|
23
|
-
day = row[
|
|
23
|
+
day = row['wager_day']
|
|
24
24
|
item.wager_day = Integer(day) if day
|
|
25
|
-
expiration = row[
|
|
25
|
+
expiration = row['expected_expiration']
|
|
26
26
|
item.expected_expiration = parse_datetime(expiration) if expiration
|
|
27
27
|
item
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def parse_datetime(str)
|
|
32
|
-
Time.strptime(str,
|
|
32
|
+
Time.strptime(str, '%Y-%m-%d %T')
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def parse_boolean(str)
|
|
36
36
|
case str
|
|
37
|
-
when
|
|
38
|
-
when
|
|
37
|
+
when 'false' then false
|
|
38
|
+
when 'true' then true
|
|
39
39
|
else
|
|
40
40
|
raise Error, "cannot parse #{str.inspect} as boolean"
|
|
41
41
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
require 'csv'
|
|
3
3
|
|
|
4
4
|
module DuolingoPersonalData
|
|
5
5
|
Language = Struct.new(:from_language, :points, :skill_learned, :total_lessons, :days_active, :last_active,
|
|
@@ -10,33 +10,33 @@ module DuolingoPersonalData
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
extend Forwardable
|
|
13
|
-
def_delegators :languages,
|
|
13
|
+
def_delegators :languages, :to_h, :[] # TODO: Add more as needed
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def languages
|
|
18
18
|
@languages ||= CSV.read(@csv_path, headers: true).map do |row|
|
|
19
|
-
language = Language.new(from_language: row[
|
|
20
|
-
points = row[
|
|
19
|
+
language = Language.new(from_language: row['from_language'])
|
|
20
|
+
points = row['points']
|
|
21
21
|
language.points = Integer(points) if points
|
|
22
|
-
skills = row[
|
|
22
|
+
skills = row['skills_learned']
|
|
23
23
|
language.skill_learned = Integer(skills) if skills
|
|
24
|
-
lessons = row[
|
|
24
|
+
lessons = row['total_lessons']
|
|
25
25
|
language.total_lessons = Integer(lessons) if lessons
|
|
26
|
-
days = row[
|
|
26
|
+
days = row['days_active']
|
|
27
27
|
language.days_active = Integer(days) if days
|
|
28
|
-
active = row[
|
|
28
|
+
active = row['last_active']
|
|
29
29
|
language.last_active = parse_datetime(active) if active
|
|
30
|
-
proficiency = row[
|
|
30
|
+
proficiency = row['prior_proficiency']
|
|
31
31
|
language.prior_proficiency = Integer(proficiency) if proficiency
|
|
32
|
-
subscribed = row[
|
|
32
|
+
subscribed = row['subscribed']
|
|
33
33
|
language.subscribed = parse_datetime(subscribed) if subscribed
|
|
34
|
-
{ row[
|
|
34
|
+
{ row['learning_language'] => language }
|
|
35
35
|
end.reduce(&:merge)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def parse_datetime(str)
|
|
39
|
-
Time.strptime(str,
|
|
39
|
+
Time.strptime(str, '%Y-%m-%d %T')
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'csv'
|
|
2
|
+
require 'forwardable'
|
|
3
3
|
|
|
4
4
|
module DuolingoPersonalData
|
|
5
5
|
LeaderboardsEntry = Struct.new(:timestamp, :tier, :score, keyword_init: true)
|
|
@@ -10,14 +10,14 @@ module DuolingoPersonalData
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
extend Forwardable
|
|
13
|
-
def_delegators :leaderboards_entries,
|
|
13
|
+
def_delegators :leaderboards_entries, :to_a, :first, :size, :[] # TODO: Add more as needed
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def leaderboards_entries
|
|
18
18
|
@leaderboards_entries ||= CSV.read(@csv_path, headers: true).map do |row|
|
|
19
|
-
LeaderboardsEntry.new(timestamp: Time.strptime(row[
|
|
20
|
-
score: Float(row[
|
|
19
|
+
LeaderboardsEntry.new(timestamp: Time.strptime(row['timestamp'], '%FT%TZ'), tier: Integer(row['tier']),
|
|
20
|
+
score: Float(row['score']))
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'json'
|
|
2
2
|
|
|
3
3
|
module DuolingoPersonalData
|
|
4
4
|
class NotifyData
|
|
@@ -7,17 +7,17 @@ module DuolingoPersonalData
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def email
|
|
10
|
-
@email ||= value_from_property(
|
|
10
|
+
@email ||= value_from_property('email')
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def device_ids
|
|
14
|
-
@device_ids ||= json_value_from_property(
|
|
14
|
+
@device_ids ||= json_value_from_property('device_ids')
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def value_from_property(property_name)
|
|
20
|
-
table.find { |row| row[
|
|
20
|
+
table.find { |row| row['property'] == property_name }['value']
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def json_value_from_property(property_name)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'csv'
|
|
2
2
|
|
|
3
3
|
module DuolingoPersonalData
|
|
4
4
|
class Profile
|
|
@@ -7,49 +7,49 @@ module DuolingoPersonalData
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def username
|
|
10
|
-
@username ||= value_from_property(
|
|
10
|
+
@username ||= value_from_property('username')
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def email
|
|
14
|
-
@email ||= value_from_property(
|
|
14
|
+
@email ||= value_from_property('email')
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def fullname
|
|
18
|
-
@fullname ||= value_from_property(
|
|
18
|
+
@fullname ||= value_from_property('fullname')
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def joined_at
|
|
22
|
-
@joined_at ||= datetime_value_from_property(
|
|
22
|
+
@joined_at ||= datetime_value_from_property('joined_at')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def ui_language
|
|
26
|
-
@ui_language ||= value_from_property(
|
|
26
|
+
@ui_language ||= value_from_property('ui_language')
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def learning_language
|
|
30
|
-
@learning_language ||= value_from_property(
|
|
30
|
+
@learning_language ||= value_from_property('learning_language')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def lingots
|
|
34
|
-
@lingots ||= integer_value_from_property(
|
|
34
|
+
@lingots ||= integer_value_from_property('lingots')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def daily_goal
|
|
38
|
-
@daily_goal ||= integer_value_from_property(
|
|
38
|
+
@daily_goal ||= integer_value_from_property('daily_goal')
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def timezone
|
|
42
|
-
@timezone ||= value_from_property(
|
|
42
|
+
@timezone ||= value_from_property('timezone')
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def avatar_url
|
|
46
|
-
@avatar_url ||= value_from_property(
|
|
46
|
+
@avatar_url ||= value_from_property('avatar_url')
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
51
|
def value_from_property(property_name)
|
|
52
|
-
table.find { |row| row[
|
|
52
|
+
table.find { |row| row['name'] == property_name }['value']
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def integer_value_from_property(property_name)
|
|
@@ -57,7 +57,7 @@ module DuolingoPersonalData
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def datetime_value_from_property(property_name)
|
|
60
|
-
Time.strptime(value_from_property(property_name),
|
|
60
|
+
Time.strptime(value_from_property(property_name), '%F %T')
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def table
|
|
@@ -5,11 +5,11 @@ module DuolingoPersonalData
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def user_id
|
|
8
|
-
@user_id ||= value_from_property(
|
|
8
|
+
@user_id ||= value_from_property('userId')
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def date_of_first_visit_to_stories
|
|
12
|
-
@date_of_first_visit_to_stories ||= datetime_value_from_property(
|
|
12
|
+
@date_of_first_visit_to_stories ||= datetime_value_from_property('dateOfFirstVisitToStories')
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
private
|
|
@@ -19,7 +19,7 @@ module DuolingoPersonalData
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def datetime_value_from_property(property_name)
|
|
22
|
-
Time.strptime(value_from_property(property_name),
|
|
22
|
+
Time.strptime(value_from_property(property_name), '%F %T')
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def table
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require 'forwardable'
|
|
2
2
|
|
|
3
3
|
module DuolingoPersonalData
|
|
4
4
|
StoryCompletion = Struct.new(:user_id, :story_id, :score, :time, keyword_init: true)
|
|
@@ -9,14 +9,14 @@ module DuolingoPersonalData
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
extend Forwardable
|
|
12
|
-
def_delegators :completions,
|
|
12
|
+
def_delegators :completions, :[] # TODO: Add more as needed
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
16
|
def completions
|
|
17
17
|
@completions ||= CSV.read(@csv_path, headers: true).map do |row|
|
|
18
|
-
StoryCompletion.new(user_id: row[
|
|
19
|
-
time: Time.strptime(row[
|
|
18
|
+
StoryCompletion.new(user_id: row['userId'], story_id: row['storyId'], score: Integer(row['score']),
|
|
19
|
+
time: Time.strptime(row['time'], '%F %T'))
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -5,27 +5,27 @@ module DuolingoPersonalData
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def disable_clubs
|
|
8
|
-
@disable_clubs ||= boolean_value_from_property(
|
|
8
|
+
@disable_clubs ||= boolean_value_from_property('disable_clubs')
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def disable_discussions
|
|
12
|
-
@disable_discussions ||= boolean_value_from_property(
|
|
12
|
+
@disable_discussions ||= boolean_value_from_property('disable_discussions')
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def disable_events
|
|
16
|
-
@disable_events ||= boolean_value_from_property(
|
|
16
|
+
@disable_events ||= boolean_value_from_property('disable_events')
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def disable_stream
|
|
20
|
-
@disable_stream ||= boolean_value_from_property(
|
|
20
|
+
@disable_stream ||= boolean_value_from_property('disable_stream')
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def disable_immersion
|
|
24
|
-
@disable_immersion ||= boolean_value_from_property(
|
|
24
|
+
@disable_immersion ||= boolean_value_from_property('disable_immersion')
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def disable_mature_words
|
|
28
|
-
@disable_mature_words ||= boolean_value_from_property(
|
|
28
|
+
@disable_mature_words ||= boolean_value_from_property('disable_mature_words')
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
@@ -33,8 +33,8 @@ module DuolingoPersonalData
|
|
|
33
33
|
def boolean_value_from_property(property_name)
|
|
34
34
|
value = value_from_property(property_name)
|
|
35
35
|
case value
|
|
36
|
-
when
|
|
37
|
-
when
|
|
36
|
+
when 'False' then false
|
|
37
|
+
when 'True' then true
|
|
38
38
|
else
|
|
39
39
|
raise Error, "cannot interpret #{value.inspect} as boolean"
|
|
40
40
|
end
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
require_relative
|
|
2
|
-
require_relative
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
1
|
+
require_relative 'duolingo_personal_data/version'
|
|
2
|
+
require_relative 'duolingo_personal_data/auth_data'
|
|
3
|
+
require_relative 'duolingo_personal_data/avatar_images'
|
|
4
|
+
require_relative 'duolingo_personal_data/blast_emails'
|
|
5
|
+
require_relative 'duolingo_personal_data/notify_data'
|
|
6
|
+
require_relative 'duolingo_personal_data/friends_follow'
|
|
7
|
+
require_relative 'duolingo_personal_data/inventory'
|
|
8
|
+
require_relative 'duolingo_personal_data/languages'
|
|
9
|
+
require_relative 'duolingo_personal_data/leaderboards'
|
|
10
|
+
require_relative 'duolingo_personal_data/profile'
|
|
11
|
+
require_relative 'duolingo_personal_data/stories'
|
|
12
|
+
require_relative 'duolingo_personal_data/story_completions'
|
|
13
|
+
require_relative 'duolingo_personal_data/teacher_privacy_settings'
|
|
14
|
+
require_relative 'duolingo_personal_data/directory'
|
|
14
15
|
|
|
15
16
|
module DuolingoPersonalData
|
|
16
17
|
class Error < StandardError; end
|
data/manifest.scm
CHANGED
|
@@ -5,7 +5,7 @@ module DuolingoPersonalData
|
|
|
5
5
|
VERSION: String
|
|
6
6
|
|
|
7
7
|
class AuthData
|
|
8
|
-
@csv_path:
|
|
8
|
+
@csv_path: String
|
|
9
9
|
@user_account_name: String?
|
|
10
10
|
@table: Array[Array[String?]]
|
|
11
11
|
@email_address: String?
|
|
@@ -13,7 +13,7 @@ module DuolingoPersonalData
|
|
|
13
13
|
@last_login_attempt_timestamp: String?
|
|
14
14
|
@last_authentication_key_refresh_timestamp: String?
|
|
15
15
|
|
|
16
|
-
def initialize: (
|
|
16
|
+
def initialize: (String csv_path) -> void
|
|
17
17
|
def user_account_name: -> String?
|
|
18
18
|
def email_address: -> String?
|
|
19
19
|
def last_update_timestamp: -> String?
|
|
@@ -27,11 +27,11 @@ module DuolingoPersonalData
|
|
|
27
27
|
|
|
28
28
|
class AvatarImages
|
|
29
29
|
extend Forwardable
|
|
30
|
-
@csv_path:
|
|
30
|
+
@csv_path: String
|
|
31
31
|
@urls: Array[URI::Generic]
|
|
32
32
|
@table: Array[Array[String?]]
|
|
33
33
|
|
|
34
|
-
def initialize: (
|
|
34
|
+
def initialize: (String csv_path) -> void
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
def urls: -> Array[URI::Generic]
|
|
@@ -39,7 +39,7 @@ module DuolingoPersonalData
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
class BlastEmails
|
|
42
|
-
@csv_path:
|
|
42
|
+
@csv_path: String
|
|
43
43
|
@email_address: String?
|
|
44
44
|
@table: Array[Array[String?]]
|
|
45
45
|
@ui_language: String?
|
|
@@ -53,7 +53,7 @@ module DuolingoPersonalData
|
|
|
53
53
|
@client: String?
|
|
54
54
|
@schools_role: Integer
|
|
55
55
|
|
|
56
|
-
def initialize: (
|
|
56
|
+
def initialize: (String csv_path) -> void
|
|
57
57
|
def email_address: -> String?
|
|
58
58
|
def ui_language: -> String?
|
|
59
59
|
def learning_language: -> String?
|
|
@@ -74,6 +74,36 @@ module DuolingoPersonalData
|
|
|
74
74
|
def table: -> Array[Array[String?]]
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
class Directory
|
|
78
|
+
@path: untyped
|
|
79
|
+
@auth_data: AuthData
|
|
80
|
+
@avatar_images: AvatarImages
|
|
81
|
+
@blast_emails: BlastEmails
|
|
82
|
+
@notify_data: untyped
|
|
83
|
+
@friends_follow: untyped
|
|
84
|
+
@inventory: untyped
|
|
85
|
+
@languages: untyped
|
|
86
|
+
@leaderboards: untyped
|
|
87
|
+
@profile: untyped
|
|
88
|
+
@stories: untyped
|
|
89
|
+
@story_completions: untyped
|
|
90
|
+
@teacher_privacy_settings: untyped
|
|
91
|
+
|
|
92
|
+
def initialize: (untyped path) -> void
|
|
93
|
+
def auth_data: -> AuthData
|
|
94
|
+
def avatar_images: -> AvatarImages
|
|
95
|
+
def blast_emails: -> BlastEmails
|
|
96
|
+
def notify_data: -> untyped
|
|
97
|
+
def friends_follow: -> untyped
|
|
98
|
+
def inventory: -> untyped
|
|
99
|
+
def languages: -> untyped
|
|
100
|
+
def leaderboards: -> untyped
|
|
101
|
+
def profile: -> untyped
|
|
102
|
+
def stories: -> untyped
|
|
103
|
+
def story_completions: -> untyped
|
|
104
|
+
def teacher_privacy_settings: -> untyped
|
|
105
|
+
end
|
|
106
|
+
|
|
77
107
|
class FriendsFollow
|
|
78
108
|
@csv_path: untyped
|
|
79
109
|
@num_following: Integer
|
|
@@ -155,12 +185,12 @@ module DuolingoPersonalData
|
|
|
155
185
|
class Leaderboards
|
|
156
186
|
extend Forwardable
|
|
157
187
|
@csv_path: untyped
|
|
158
|
-
@
|
|
188
|
+
@leaderboards_entries: Array[LeaderboardsEntry]
|
|
159
189
|
|
|
160
190
|
def initialize: (untyped csv_path) -> void
|
|
161
191
|
|
|
162
192
|
private
|
|
163
|
-
def
|
|
193
|
+
def leaderboards_entries: -> Array[LeaderboardsEntry]
|
|
164
194
|
end
|
|
165
195
|
|
|
166
196
|
class NotifyData
|
metadata
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: duolingo_personal_data
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gemmaro
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-06-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Duolingo Personal Data gem is for loading Duolingo Personal Data, which
|
|
14
|
-
can be acquired at
|
|
14
|
+
can be acquired at https://drive-thru.duolingo.com/.
|
|
15
15
|
email:
|
|
16
16
|
- gemmaro.dev@gmail.com
|
|
17
17
|
executables: []
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- lib/duolingo_personal_data/auth_data.rb
|
|
43
43
|
- lib/duolingo_personal_data/avatar_images.rb
|
|
44
44
|
- lib/duolingo_personal_data/blast_emails.rb
|
|
45
|
+
- lib/duolingo_personal_data/directory.rb
|
|
45
46
|
- lib/duolingo_personal_data/friends_follow.rb
|
|
46
47
|
- lib/duolingo_personal_data/inventory.rb
|
|
47
48
|
- lib/duolingo_personal_data/languages.rb
|
|
@@ -61,6 +62,7 @@ metadata:
|
|
|
61
62
|
homepage_uri: https://gitlab.com/gemmaro/ruby-duolingo-personal-data
|
|
62
63
|
source_code_uri: https://gitlab.com/gemmaro/ruby-duolingo-personal-data
|
|
63
64
|
changelog_uri: https://gitlab.com/gemmaro/ruby-duolingo-personal-data/-/blob/main/CHANGELOG.md
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
64
66
|
post_install_message:
|
|
65
67
|
rdoc_options: []
|
|
66
68
|
require_paths:
|
|
@@ -79,5 +81,5 @@ requirements: []
|
|
|
79
81
|
rubygems_version: 3.1.6
|
|
80
82
|
signing_key:
|
|
81
83
|
specification_version: 4
|
|
82
|
-
summary: Library for Duolingo personal data
|
|
84
|
+
summary: Library for Duolingo personal data
|
|
83
85
|
test_files: []
|