ordlite 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,218 @@
1
+ module OrdDb
2
+
3
+ class Importer
4
+ Inscribe = Model::Inscribe
5
+ Blob = Model::Blob
6
+ Collection = Model::Collection
7
+
8
+
9
+
10
+ def import_collection_csv( path,
11
+ name:,
12
+ content: true )
13
+ ## or use
14
+ ## import_collection( format: 'csv') - why? why not?
15
+ recs = read_csv( path )
16
+ puts " #{recs.size} inscribe id(s)"
17
+
18
+ col = Collection.find_by( name: name )
19
+ if col && col.items.count > 0
20
+ puts "!! WARN - collection already in db; delete first to reimport"
21
+ return
22
+ elsif col
23
+ ## do nothing; (re)use collection record; add items
24
+ else
25
+ col = Collection.create(
26
+ name: name
27
+ ## max: recs.size ## auto-add max - why? why not?
28
+ )
29
+ end
30
+
31
+ recs.each_with_index do |rec,i|
32
+ id = rec['id']
33
+ name = rec['name'] || rec['title']
34
+ puts "==> #{i+1}/#{recs.size} >#{name}< @ #{id}..."
35
+
36
+ col.items.create( pos: i,
37
+ inscribe_id: id,
38
+ name: name )
39
+
40
+ _import( id, content: content )
41
+ end
42
+ end
43
+
44
+
45
+ def import_collection_inscriptions( path,
46
+ name:,
47
+ content: true )
48
+ recs = read_json( path )
49
+ puts " #{recs.size} inscribe id(s)"
50
+
51
+ col = Collection.find_by( name: name )
52
+ if col && col.items.count > 0
53
+ puts "!! WARN - collection already in db; delete first to reimport"
54
+ return
55
+ elsif col
56
+ ## do nothing; (re)use collection record; add items
57
+ else
58
+ col = Model::Collection.create(
59
+ name: name
60
+ ## max: recs.size ## auto-add max - why? why not?
61
+ )
62
+ end
63
+
64
+ recs.each_with_index do |rec,i|
65
+ id = rec['id']
66
+ meta = rec['meta']
67
+ name = meta['name']
68
+ puts "==> #{i+1}/#{recs.size} >#{name}< @ #{id}..."
69
+
70
+ col.items.create( pos: i,
71
+ inscribe_id: id,
72
+ name: name )
73
+
74
+ _import( id, content: content )
75
+ end
76
+ end
77
+
78
+
79
+ def import_collection( path, content: true )
80
+ data = read_json( path )
81
+
82
+ meta = data['collection']
83
+ pp meta
84
+
85
+ name = meta['name']
86
+
87
+ col = Collection.find_by( name: name )
88
+ if col
89
+ puts "!! WARN - collection already in db; delete first to reimport"
90
+ return
91
+ end
92
+
93
+ col = Collection.create(
94
+ name: name,
95
+ desc: meta['description'],
96
+ max: meta['max_supply']
97
+ )
98
+
99
+ items = data['items']
100
+ puts " #{items.size} inscribe id(s)"
101
+ items.each_with_index do |rec,i|
102
+ id = rec['inscription_id']
103
+ name = rec['name']
104
+ puts "==> #{i+1}/#{items.size} @ #{id}..."
105
+
106
+ col.items.create( pos: i,
107
+ inscribe_id: id,
108
+ name: name )
109
+
110
+ _import( id, content: content )
111
+ end
112
+ end
113
+
114
+
115
+ def import_csv( path, content: true )
116
+ recs = read_csv( path )
117
+ puts " #{recs.size} inscribe id(s)"
118
+ #=> 1000 inscribe id(s)
119
+
120
+ recs.each_with_index do |rec,i|
121
+ id = rec['id']
122
+ puts "==> #{i+1}/#{rec.size} @ #{id}..."
123
+
124
+ _import( id, content: content )
125
+ end
126
+ end # method import_csv
127
+
128
+ def import( id_or_ids, content: true )
129
+ if id_or_ids.is_a?( String )
130
+ id = id_or_ids
131
+ _import( id, content: content )
132
+ else ## assume array
133
+ ids = id_or_ids
134
+ ids.each do |id|
135
+ _import( id, content: content )
136
+ end
137
+ end
138
+ end
139
+
140
+ def _import( id, content: true )
141
+ ## check if inscription / inscribe is already in db?
142
+ inscribe = Inscribe.find_by( id: id )
143
+ if inscribe ## already in db; dump record
144
+ ## pp inscribe
145
+ else ## fetch via ordinals.com api and update db
146
+ data = Ordinals.inscription( id )
147
+
148
+ pp data
149
+ Inscribe.create_from_api( data )
150
+ end
151
+
152
+ if content
153
+ ## check if (content) blob is already in db?
154
+ blob = Blob.find_by( id: id )
155
+ if blob ## already in db; do nothing
156
+ else ## fetch via ordinals.com api and update db
157
+ content = Ordinals.content( id )
158
+
159
+ puts " content-type: #{content.type}"
160
+ puts " content-length: #{content.length}"
161
+
162
+ Blob.create( id: id, content: content.data )
163
+ end
164
+ end
165
+ end
166
+
167
+ end # class Importer
168
+
169
+
170
+
171
+ ###
172
+ ## convenience helpers
173
+
174
+ def self.importer ## "default" importer
175
+ @importer ||= Importer.new
176
+ end
177
+
178
+ def self.import( id_or_ids, content: true )
179
+ importer.import( id_or_ids, content: content )
180
+ end
181
+
182
+
183
+ def self.import_csv( path, content: true )
184
+ importer.import_csv( path, content: content )
185
+ end
186
+
187
+
188
+ def self.import_collection( path, content: true )
189
+ importer.import_collection( path, content: content )
190
+ end
191
+
192
+ def self.import_collection_inscriptions( path,
193
+ name:,
194
+ content: true )
195
+ importer.import_collection_inscriptions( path,
196
+ name: name,
197
+ content: content )
198
+ end
199
+
200
+ def self.import_collection_csv( path,
201
+ name:,
202
+ content: true )
203
+ importer.import_collection_csv( path,
204
+ name: name,
205
+ content: content )
206
+ end
207
+
208
+
209
+ module Model
210
+ class Inscribe
211
+ def self.import( id_or_ids, content: true )
212
+ OrdDb.importer.import( id_or_ids, content: content )
213
+ end
214
+ end # class Inscribe
215
+ end # module Model
216
+
217
+
218
+ end # module OrdDb
@@ -4,6 +4,10 @@ module OrdDb
4
4
 
5
5
  class Blob < ActiveRecord::Base
6
6
  belongs_to :inscribe, foreign_key: 'id'
7
+
8
+ def text
9
+ content.force_encoding(Encoding::UTF_8)
10
+ end
7
11
  end # class Blob
8
12
 
9
13
  end # module Model
@@ -0,0 +1,14 @@
1
+ module OrdDb
2
+ module Model
3
+
4
+ class Collection < ActiveRecord::Base
5
+ has_many :items
6
+ ## -> { order('pos') }
7
+ ## note: default_scope (order)
8
+ ## will break all count queries and more
9
+ ## thus - no "magic" - always sort if pos order required!!!
10
+ has_many :inscribes, :through => :items
11
+ end # class Collection
12
+
13
+ end # module Model
14
+ end # module OrdDb
@@ -0,0 +1,25 @@
1
+
2
+ module OrdDb
3
+ module Model
4
+
5
+ class Factory < ActiveRecord::Base
6
+ self.table_name = 'factories' ## non-standard plural (factory/factories)
7
+
8
+ belongs_to :inscribe
9
+
10
+ has_many :inscriberefs ## join table (use habtm - why? why not?)
11
+ ## -> { order('pos') }
12
+ ## note: default_scope (order)
13
+ ## will break all count queries and more
14
+ ## thus - no "magic" - always sort if pos order required!!!
15
+
16
+ has_many :layers, :through => :inscriberefs,
17
+ :source => :inscribe
18
+
19
+ has_many :generatives
20
+ has_many :inscribes, :through => :generatives
21
+ end # class Factory
22
+
23
+ end # module Model
24
+ end # module OrdDb
25
+
@@ -12,12 +12,31 @@ Prop = ConfDb::Model::Prop
12
12
 
13
13
  class Inscribe < ActiveRecord::Base ; end
14
14
  class Blob < ActiveRecord::Base ; end
15
-
16
- end
15
+ class Collection < ActiveRecord::Base ; end
16
+ class Item < ActiveRecord::Base ## change to CollectionItem - why? why not?
17
+ belongs_to :collection
18
+ belongs_to :inscribe
19
+ end
20
+
21
+ class Factory < ActiveRecord::Base ; end
22
+ class Generative < ActiveRecord::Base ; end
23
+
24
+ ### join tables - add inline here - why? why not?
25
+ ## rename to CollectionLayer? or
26
+ ## CollectionInscribeRef? or
27
+ ## Layerref? or
28
+ ## InscribeRef?
29
+ ## FactoryItem ????
30
+ class Inscriberef < ActiveRecord::Base
31
+ belongs_to :factory
32
+ belongs_to :inscribe
33
+ end
34
+
35
+ end # module Model
17
36
 
18
37
  # note: convenience alias for Model
19
38
  # lets you use include OrdDb::Models
20
39
  Models = Model
21
- end # module # OrdDb
40
+ end # module OrdDb
22
41
 
23
42
 
@@ -0,0 +1,12 @@
1
+
2
+ module OrdDb
3
+ module Model
4
+
5
+ class Generative < ActiveRecord::Base
6
+ belongs_to :inscribe, foreign_key: 'id'
7
+ belongs_to :factory
8
+ end # class Generative
9
+
10
+ end # module Model
11
+ end # module OrdDb
12
+
@@ -4,12 +4,17 @@ module OrdDb
4
4
 
5
5
  class Inscribe < ActiveRecord::Base
6
6
  has_one :blob, foreign_key: 'id'
7
+
8
+ has_one :factory ## optional (auto-added via og/orc-721 deploy)
9
+ has_one :generative, foreign_key: 'id' ## optional (auto-added via og/orc-721 deploy)
10
+
11
+ ## convernience helper
12
+ ## forward to blob.content
13
+ ## blob.content - encoding is BINARY (ASCII-7BIT)
14
+ ## blob.text - force_encoding is UTF-8 (return a copy)
15
+ def content() blob.content; end
16
+ def text() blob.text; end
7
17
 
8
- def content
9
- ## convernience helper
10
- ## forward to blob.content
11
- blob.content
12
- end
13
18
 
14
19
  ################################
15
20
  ### scope like helpers
@@ -54,6 +59,228 @@ SQL
54
59
 
55
60
  joins(:blob).where( where_clause ).order( 'num' )
56
61
  end
62
+
63
+ def self.sub1k() where( 'num < 1000' ); end
64
+ def self.sub2k() where( 'num < 2000' ); end
65
+ def self.sub10k() where( 'num < 10000' ); end
66
+ def self.sub20k() where( 'num < 20000' ); end
67
+ def self.sub100k() where( 'num < 100000' ); end
68
+ def self.sub1m() where( 'num < 1000000' ); end
69
+ def self.sub2m() where( 'num < 2000000' ); end
70
+ def self.sub10m() where( 'num < 10000000' ); end
71
+ def self.sub20m() where( 'num < 20000000' ); end
72
+ def self.sub21m() where( 'num < 21000000' ); end
73
+
74
+
75
+ def self.largest
76
+ order( 'bytes DESC' )
77
+ end
78
+
79
+ def self.address_counts
80
+ group( 'address' )
81
+ .order( Arel.sql( 'COUNT(*) DESC')).count
82
+ end
83
+
84
+ def self.block_counts
85
+ group( 'block' )
86
+ .order( 'block').count
87
+ end
88
+
89
+ def self.block_with_timestamp_counts
90
+ group( Arel.sql( "block || ' @ ' || date" ))
91
+ .order( Arel.sql( "block || ' @ ' || date" ) ).count
92
+ end
93
+
94
+ def self.content_type_counts
95
+ group( 'content_type' )
96
+ .order( Arel.sql( 'COUNT(*) DESC, content_type')).count
97
+ end
98
+
99
+
100
+ def self.date_counts
101
+ ## note: strftime is SQLite specific/only!!!
102
+ group( Arel.sql("strftime('%Y-%m-%d', date)"))
103
+ .order( Arel.sql("strftime('%Y-%m-%d', date)")).count
104
+ end
105
+
106
+ def self.month_counts
107
+ ## note: strftime is SQLite specific/only!!!
108
+ group( Arel.sql("strftime('%Y-%m', date)"))
109
+ .order( Arel.sql("strftime('%Y-%m', date)")).count
110
+ end
111
+
112
+ def self.hour_counts
113
+ ## note: strftime is SQLite specific/only!!!
114
+ group( Arel.sql("strftime('%Y-%m-%d %Hh', date)"))
115
+ .order( Arel.sql("strftime('%Y-%m-%d %Hh', date)")).count
116
+ end
117
+
118
+
119
+ class << self
120
+ alias_method :biggest, :largest
121
+ alias_method :counts_by_address, :address_counts
122
+ alias_method :counts_by_content_type, :content_type_counts
123
+ alias_method :counts_by_date, :date_counts
124
+ alias_method :counts_by_day, :date_counts
125
+ alias_method :counts_by_month, :month_counts
126
+ alias_method :counts_by_hour, :hour_counts
127
+ alias_method :counts_by_block, :block_counts
128
+ alias_method :counts_by_block_with_timestamp, :block_with_timestamp_counts
129
+ end
130
+
131
+
132
+ def self.text
133
+ ## note: for now include:
134
+ ## - text/plain (all variants)
135
+ ## - text/json (all variants)
136
+ ## - text/markdown
137
+ where( content_type:
138
+ ['text/plain',
139
+ 'text/plain;charset=utf-8',
140
+ 'text/markdown',
141
+ 'application/json',
142
+ ]
143
+ )
144
+ end
145
+ def self.png() where( content_type: 'image/png' ); end
146
+
147
+ ###
148
+ ## add support for ordinals.com api txt (headers format)
149
+
150
+
151
+ def self.create_from_api( data ) create( _parse_api( data )); end
152
+ class << self
153
+ alias_method :create_from_cache, :create_from_api ## add alias - why? why not?
154
+ end
155
+
156
+
157
+ def self._parse_api( data ) ## parse api json data
158
+ ## num via title
159
+ attributes = {
160
+ id: data['id'],
161
+ num: _title_to_num( data['title'] ),
162
+ bytes: _content_length_to_bytes( data['content-length'] ),
163
+ sat: data['sat'].to_i(10),
164
+ content_type: data['content-type'],
165
+ block: data['genesis-height'].to_i(10),
166
+ fee: data['genesis-fee'].to_i(10),
167
+ tx: data['genesis-transaction'],
168
+ address: data['address'],
169
+ output: data['output'],
170
+ value: data['output-value'].to_i(10),
171
+ offset: data['offset'].to_i(10),
172
+ # "2023-06-01 05:00:57 UTC"
173
+ date: DateTime.strptime( data['timestamp'],
174
+ '%Y-%m-%d %H:%M:%S %z')
175
+ }
176
+
177
+ attributes
178
+ end
179
+
180
+
181
+ ## "title": "Inscription 9992615",
182
+ TITLE_RX = /^Inscription (?<num>[0-9]+)$/i
183
+
184
+ def self._title_to_num( str )
185
+ if m=TITLE_RX.match( str )
186
+ m[:num].to_i(10) ## use base 10
187
+ else
188
+ puts "!! ERROR - no inscribe num found in title >#{str}<"
189
+ exit 1 ## not found - raise exception - why? why not?
190
+ end
191
+ end
192
+
193
+ CONTENT_LENGTH_RX = /^(?<num>[0-9]+) bytes$/i
194
+
195
+ def self._content_length_to_bytes( str )
196
+ if m=CONTENT_LENGTH_RX.match( str )
197
+ m[:num].to_i(10) ## use base 10
198
+ else
199
+ puts "!! ERROR - bytes found in content lenght >#{str}<"
200
+ exit 1 ## not found - raise exception - why? why not?
201
+ end
202
+ end
203
+
204
+
205
+
206
+ ###
207
+ # instance methods
208
+ def extname
209
+ ## map mime type to file extname
210
+ ## see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
211
+ ## for real-world usage, see https://dune.com/dgtl_assets/bitcoin-ordinals-analysis
212
+ ## https://github.com/casey/ord/blob/master/src/media.rs
213
+
214
+ if content_type.start_with?( 'text/plain' )
215
+ '.txt'
216
+ elsif content_type.start_with?( 'text/markdown' )
217
+ '.md'
218
+ elsif content_type.start_with?( 'text/html' )
219
+ '.html'
220
+ elsif content_type.start_with?( 'text/javascript' ) ||
221
+ content_type.start_with?( 'application/javascript' )
222
+ ## note: application/javascript is considered bad practice/legacy
223
+ '.js'
224
+ elsif content_type.start_with?( 'image/png' )
225
+ ## Portable Network Graphics (PNG)
226
+ '.png'
227
+ elsif content_type.start_with?( 'image/jpeg' )
228
+ ## Joint Photographic Expert Group image (JPEG)
229
+ '.jpg' ## use jpeg - why? why not?
230
+ elsif content_type.start_with?( 'image/webp' )
231
+ ## Web Picture format (WEBP)
232
+ '.webp' ## note: no three-letter extension available
233
+ elsif content_type.start_with?( 'image/svg' )
234
+ ## Scalable Vector Graphics (SVG)
235
+ '.svg'
236
+ elsif content_type.start_with?( 'image/gif' )
237
+ ## Graphics Interchange Format (GIF)
238
+ '.gif'
239
+ elsif content_type.start_with?( 'image/avif' )
240
+ ## AV1 Image File Format (AVIF)
241
+ '.avif'
242
+ elsif content_type.start_with?( 'application/epub' )
243
+ '.epub'
244
+ elsif content_type.start_with?( 'application/pdf' )
245
+ '.pdf'
246
+ elsif content_type.start_with?( 'application/json' )
247
+ '.json'
248
+ elsif content_type.start_with?( 'application/pgp-signature' )
249
+ '.sig'
250
+ elsif content_type.start_with?( 'audio/mpeg' )
251
+ '.mp3'
252
+ elsif content_type.start_with?( 'audio/midi' )
253
+ '.midi'
254
+ elsif content_type.start_with?( 'video/mp4' )
255
+ '.mp4'
256
+ elsif content_type.start_with?( 'video/webm' )
257
+ '.wepm'
258
+ elsif content_type.start_with?( 'audio/mod' )
259
+ ## is typo? possible? only one inscription in 20m?
260
+ '.mod' ## check/todo/fix if is .wav??
261
+ else
262
+ puts "!! ERROR - no file extension configured for content type >#{content_type}<; sorry:"
263
+ pp self
264
+ exit 1
265
+ end
266
+ end
267
+
268
+ def export_path ## default export path
269
+ numstr = "%08d" % num ### e.g. 00000001
270
+ "./tmp/#{numstr}#{extname}"
271
+ end
272
+ def export( path=export_path )
273
+ if blob
274
+ write_blob( path, blob.content )
275
+ else
276
+ ## todo/fix: raise exception - no content
277
+ puts "!! ERROR - inscribe has no content (blob); sorry:"
278
+ pp self
279
+ exit 1
280
+ end
281
+ end
282
+
283
+
57
284
  end # class Inscribe
58
285
 
59
286
  end # module Model
@@ -87,7 +87,7 @@ create_table :inscribes, :id => :string do |t|
87
87
  ## what is location ???
88
88
  ## "location": "0a3a4dbf6630338bc4df8e36bd081f8f7d2dee9441131cb03a18d43eb4882d5c:0:0",
89
89
 
90
- ## timestamp at last
90
+ ## timestamp last
91
91
  t.timestamps
92
92
  end
93
93
 
@@ -99,14 +99,117 @@ create_table :blobs, :id => :string do |t|
99
99
  ## t.string :id, null: false, index: { unique: true, name: 'blob_uuids' }
100
100
 
101
101
  t.binary :content, null: false
102
+ t.string :sha256 ## sha256 hash
103
+ t.string :md5 ## md5 hash - add why? why not?
102
104
 
103
- ## timestamp at last
105
+ ## timestamp last
104
106
  t.timestamps
105
107
  end
106
108
 
107
109
 
110
+ =begin
111
+ "name": "Planetary Ordinals",
112
+ "inscription_icon": "98da33abe2045ec1421fcf1bc376dea5beb17ded15aa70ca5da490f50d95a6d9i0",
113
+ "supply": "69",
114
+ "slug": "planetary-ordinals",
115
+ "description": "",
116
+ "twitter_link": "https://twitter.com/ordinalswallet",
117
+ "discord_link": "https://discord.com/invite/ordinalswallet",
118
+ "website_link": ""
119
+ =end
120
+
121
+ create_table :collections do |t|
122
+ t.string :name, null: false
123
+ t.string :slug
124
+ t.text :desc # description
125
+ t.integer :max # supply
126
+ t.string :icon_id ## rename to inscribe_icon_id or such - why? why not?
127
+ ## add twitter_link, discord_link, website_link - why? why not?
128
+
129
+ ## if on-chain and metadata inscribed - add why? why not??
130
+ ## t.string :source_id, null: false ## foreign key reference
131
+
132
+ ## timestamp last
133
+ t.timestamps
134
+ end
135
+
136
+ create_table :items do |t|
137
+ t.integer :collection_id, null: false
138
+ t.string :inscribe_id, null: false
139
+ t.integer :pos, null: false
140
+ t.string :name
141
+
142
+ ## timestamp last
143
+ t.timestamps
144
+
145
+ ## todo/fix: add unique index for :pos+:collection_id !!!
146
+ end
147
+
148
+
149
+
150
+ ###
151
+ # generative (collection) factory
152
+ create_table :factories, :id => :string do |t|
153
+ t.string :name
154
+ t.integer :max # max limit
155
+ t.integer :maxblock # max block limit
156
+ t.string :dim # dimension e.g. 24x24 (in px)
157
+
158
+ t.string :inscribe_id, null: false ## foreign key reference
159
+
160
+ ## timestamp last
161
+ t.timestamps
162
+ end
163
+
164
+ #####
165
+ ## join table (factory has_many modules)
166
+ ## rename to layer / sprites / blocks / tiles / modules / submodules / subs / mods / ...etc - why? why not?
167
+ ## layerlists or inscribelists or ???
168
+ ## change/rename to factory_items or layer_items or such?
169
+ create_table :inscriberefs, :id => false do |t|
170
+ t.string :factory_id, null: false
171
+ t.string :inscribe_id, null: false
172
+ t.integer :pos, null: false ## position (index) in list (starting at 0)
173
+ ## todo/fix: make factory_id + inscribe_id + pos unique index - why? why not?
174
+
175
+ ## timestamp last
176
+ t.timestamps
177
+ end
178
+
179
+
180
+ create_table :generatives, :id => :string do |t|
181
+ t.string :factory_id, null: false
182
+ t.string :g, null: false ## use space separated numbers - why? why not?
183
+ t.binary :content ### optional for now - why? why not?
184
+
185
+ ## timestamp last
186
+ t.timestamps
187
+ end
188
+
189
+
190
+
191
+
108
192
  end # block Schema.define
109
193
 
110
194
  end # method up
111
195
  end # class CreateDb
196
+
197
+ ###
198
+ # migrations helpers
199
+ class AddGeneratives
200
+
201
+ def up
202
+ ActiveRecord::Schema.define do
203
+ create_table :generatives, :id => :string do |t|
204
+ t.string :factory_id, null: false
205
+ t.string :g, null: false ## use space separated numbers - why? why not?
206
+ t.binary :content ### optional for now - why? why not?
207
+
208
+ ## timestamp last
209
+ t.timestamps
210
+ end
211
+ end # block Schema.define
212
+ end # method up
213
+ end # class AddGeneratives
214
+
112
215
  end # module OrdDb
@@ -3,8 +3,8 @@ module Ordlite
3
3
 
4
4
  # sync version w/ sport.db n friends - why? why not?
5
5
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
6
- MINOR = 1
7
- PATCH = 2
6
+ MINOR = 2
7
+ PATCH = 1
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9
 
10
10
  def self.version