ordlite 0.1.2 → 0.2.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,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