datapipeline 2.0.0: Gem aus dem Projekt etl herausgelöst
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
#
|
||||
# Nur neue Buchungsbewegungen exportieren, Zeile für Zeile (Streaming)
|
||||
# Ein zweiter Lauf ohne neue Daten schreibt nichts.
|
||||
#
|
||||
# bin/etl run examples/bewegungen_inkrementell.rb [--out=tmp]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
DATA = File.expand_path('data', __dir__)
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
DataPipeline.new do
|
||||
streamed
|
||||
|
||||
# höchste id des letzten erfolgreichen Laufs, gespeichert in tmp/.state.json
|
||||
stand = watermark :bewegungen, column: 'id', file: "#{out}/.state.json", initial: 0
|
||||
|
||||
# bei einer Datenbank gehört der Filter ins SQL: where id > $1, params: [stand.value]
|
||||
read container(:json, file: "#{DATA}/bewegungen.ndjson")
|
||||
filter { |row| row['id'] > stand.value.to_i }
|
||||
map { |row| row.merge('soll_haben' => row['betrag'].negative? ? 'H' : 'S', 'betrag' => row['betrag'].abs) }
|
||||
|
||||
write container(:json,
|
||||
file: "#{out}/bewegungen_#{Time.now.strftime('%Y%m%d_%H%M%S')}.ndjson"
|
||||
)
|
||||
|
||||
on_finish { |stats| puts "neu: #{stats.written}, Stand jetzt: #{stand.current}" }
|
||||
end.run
|
||||
@@ -0,0 +1,4 @@
|
||||
{"id":1,"konto":"4000","betrag":120.0}
|
||||
{"id":2,"konto":"4000","betrag":-20.0}
|
||||
{"id":3,"konto":"4400","betrag":75.5}
|
||||
{"id":4,"konto":"4000","betrag":10.0}
|
||||
@@ -0,0 +1,3 @@
|
||||
artikel,menge
|
||||
A-100,5
|
||||
B-200,2
|
||||
|
@@ -0,0 +1,3 @@
|
||||
artikel,menge
|
||||
A-100,7
|
||||
C-300,1
|
||||
|
@@ -0,0 +1,10 @@
|
||||
kundennr;name;land;status;umsatz
|
||||
100001;Müller GmbH;DE;aktiv;12500.50
|
||||
100002;Huber & Partner;AT;aktiv;8300
|
||||
100003;Swiss Trade AG;CH;aktiv;22000.75
|
||||
100004;Alte Firma KG;DE;inaktiv;0
|
||||
100005; ;DE;aktiv;100
|
||||
10006;Kurz;DE;aktiv;50
|
||||
100007;Nowhere Ltd;XX;aktiv;10
|
||||
100008;Dupont SARL;FR;aktiv;abc
|
||||
100009;Rossi Srl;IT;aktiv;4100.10
|
||||
|
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "code": "DE", "name": "Deutschland", "eu": true },
|
||||
{ "code": "AT", "name": "Österreich", "eu": true },
|
||||
{ "code": "CH", "name": "Schweiz", "eu": false },
|
||||
{ "code": "FR", "name": "Frankreich", "eu": true },
|
||||
{ "code": "IT", "name": "Italien", "eu": true }
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{ "nr": "R-1001", "datum": "2025-11-03", "kunde": "100001",
|
||||
"positionen": [{ "text": "Beratung", "menge": 2, "preis": 150.0 },
|
||||
{ "text": "Reisekosten", "menge": 1, "preis": 80.5 }] },
|
||||
{ "nr": "R-1002", "datum": "2025-11-05", "kunde": "100002",
|
||||
"positionen": [{ "text": "Jahreslizenz Software mit Wartung und Support", "menge": 1, "preis": 1200 }] }
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
#
|
||||
# Kleine Demo-API für die REST-Beispiele, ohne Internet und ohne weitere Gems
|
||||
#
|
||||
# ruby examples/demo_api.rb # http://127.0.0.1:4567, Token: demo-token
|
||||
#
|
||||
# Endpunkte (alle mit Authorization: Bearer demo-token, sonst 401)
|
||||
# GET /api/kunden?page=1&per_page=3 {"data": [...], "page": 1} die 2. Seite antwortet einmal mit 503
|
||||
# GET /api/laender [{"code": "DE", "name": "Deutschland"}, ...]
|
||||
# GET /api/bestellungen[?cursor=n] {"items": [...], "next": "/api/bestellungen?cursor=3"} oder "next": null
|
||||
# POST /api/import ein Objekt oder ein Array; 422 ohne kundennr/name
|
||||
# GET /api/import alles, was angenommen wurde
|
||||
# PUT /api/archiv/<id> ein Objekt ablegen
|
||||
# GET /api/archiv alles im Archiv
|
||||
#
|
||||
require 'json'
|
||||
require 'socket'
|
||||
require 'uri'
|
||||
|
||||
class DemoApi
|
||||
TOKEN = 'demo-token'.freeze
|
||||
|
||||
KUNDEN = [
|
||||
{ 'id' => 1, 'name' => 'Müller GmbH', 'land' => 'DE', 'umsatz' => '12500.50' },
|
||||
{ 'id' => 2, 'name' => 'Huber & Partner', 'land' => 'AT', 'umsatz' => '8300' },
|
||||
{ 'id' => 3, 'name' => 'Swiss Trade AG', 'land' => 'CH', 'umsatz' => '22000.75' },
|
||||
{ 'id' => 4, 'name' => 'Rossi Srl', 'land' => 'IT', 'umsatz' => '4100.10' },
|
||||
{ 'id' => 5, 'name' => 'Dupont SARL', 'land' => 'FR', 'umsatz' => 'n/a' },
|
||||
{ 'id' => 6, 'name' => 'Nordic AB', 'land' => 'SE', 'umsatz' => '990' },
|
||||
{ 'id' => 7, 'name' => 'Vaduz Treuhand', 'land' => 'LI', 'umsatz' => '15000' }
|
||||
].freeze
|
||||
|
||||
LAENDER = [
|
||||
{ 'code' => 'DE', 'name' => 'Deutschland' }, { 'code' => 'AT', 'name' => 'Österreich' },
|
||||
{ 'code' => 'CH', 'name' => 'Schweiz' }, { 'code' => 'IT', 'name' => 'Italien' },
|
||||
{ 'code' => 'FR', 'name' => 'Frankreich' }, { 'code' => 'LI', 'name' => 'Liechtenstein' }
|
||||
].freeze
|
||||
|
||||
BESTELLUNGEN = (1..5).map { |i| { 'id' => i, 'kunde' => (i % 3) + 1, 'betrag' => i * 100.0 } }.freeze
|
||||
|
||||
attr_reader :imported, :archive
|
||||
|
||||
def self.start(port: 4567) = new(port).tap(&:start)
|
||||
|
||||
def initialize port
|
||||
@server = TCPServer.new('127.0.0.1', port)
|
||||
@imported = []
|
||||
@archive = {}
|
||||
@failed_once = false
|
||||
end
|
||||
|
||||
def url = "http://127.0.0.1:#{@server.addr[1]}"
|
||||
|
||||
def start
|
||||
@thread = Thread.new { loop { Thread.new(@server.accept) { |client| serve(client) } } }
|
||||
self
|
||||
end
|
||||
|
||||
def stop
|
||||
@thread&.kill
|
||||
@server.close
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serve client
|
||||
method, target = client.gets.to_s.split
|
||||
return unless method
|
||||
|
||||
headers = {}
|
||||
while (line = client.gets) && line != "\r\n"
|
||||
key, value = line.split(':', 2)
|
||||
headers[key.downcase] = value.strip
|
||||
end
|
||||
body = headers['content-length'] ? client.read(headers['content-length'].to_i) : ''
|
||||
path, query = target.split('?', 2)
|
||||
params = URI.decode_www_form(query.to_s).to_h
|
||||
|
||||
status, payload = headers['authorization'] == "Bearer #{TOKEN}" ? route(method, path, params, body) : [401, { 'error' => 'token' }]
|
||||
text = JSON.generate(payload)
|
||||
client.write "HTTP/1.1 #{status} X\r\nContent-Type: application/json\r\nContent-Length: #{text.bytesize}\r\n" \
|
||||
"Connection: close\r\n\r\n#{text}"
|
||||
rescue StandardError => e
|
||||
warn "DemoApi: #{e.class}: #{e.message}"
|
||||
ensure
|
||||
client.close
|
||||
end
|
||||
|
||||
def route method, path, params, body
|
||||
case [method, path]
|
||||
in ['GET', '/api/kunden'] then kunden(params)
|
||||
in ['GET', '/api/laender'] then [200, LAENDER]
|
||||
in ['GET', '/api/bestellungen'] then bestellungen(params)
|
||||
in ['POST', '/api/import'] then import(JSON.parse(body))
|
||||
in ['GET', '/api/import'] then [200, @imported]
|
||||
in ['PUT', %r{\A/api/archiv/\w+\z}] then archivieren(path.split('/').last, JSON.parse(body))
|
||||
in ['GET', '/api/archiv'] then [200, @archive.values]
|
||||
else [404, { 'error' => "#{method} #{path}" }]
|
||||
end
|
||||
end
|
||||
|
||||
def kunden params
|
||||
page = params.fetch('page', '1').to_i
|
||||
per_page = params.fetch('per_page', '3').to_i
|
||||
if page == 2 && !@failed_once
|
||||
@failed_once = true
|
||||
return [503, { 'error' => 'kurz nicht verfügbar' }]
|
||||
end
|
||||
[200, { 'data' => KUNDEN.drop((page - 1) * per_page).first(per_page), 'page' => page }]
|
||||
end
|
||||
|
||||
def bestellungen params
|
||||
cursor = params.fetch('cursor', '0').to_i
|
||||
items = BESTELLUNGEN.drop(cursor).first(2)
|
||||
following = cursor + 2 < BESTELLUNGEN.size ? "/api/bestellungen?cursor=#{cursor + 2}" : nil
|
||||
[200, { 'items' => items, 'next' => following }]
|
||||
end
|
||||
|
||||
def archivieren id, data
|
||||
@archive[id] = data
|
||||
[200, { 'ok' => true }]
|
||||
end
|
||||
|
||||
def import data
|
||||
rows = data.is_a?(Array) ? data : [data]
|
||||
invalid = rows.reject { |r| r['kundennr'] && r['name'] }
|
||||
return [422, { 'error' => 'kundennr und name sind Pflicht', 'zeilen' => invalid }] if invalid.any?
|
||||
|
||||
@imported.concat(rows)
|
||||
[201, { 'angenommen' => rows.size }]
|
||||
end
|
||||
end
|
||||
|
||||
if $PROGRAM_NAME == __FILE__
|
||||
api = DemoApi.start(port: ENV.fetch('PORT', 4567).to_i)
|
||||
puts "Demo-API auf #{api.url}, Token #{DemoApi::TOKEN} (Ctrl-C beendet)"
|
||||
sleep
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
#
|
||||
# Eigenes Plugin: liest alle CSV-Dateien eines Verzeichnisses als eine Quelle
|
||||
#
|
||||
# bin/etl run examples/eigenes_plugin.rb [--out=tmp]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
DATA = File.expand_path('data', __dir__)
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
class VerzeichnisStorage < DataPipeline::StoragePlugin
|
||||
def initialize dir:, pattern: '*.csv', **csv_options
|
||||
@dir = dir
|
||||
@pattern = pattern
|
||||
@csv_options = csv_options
|
||||
end
|
||||
|
||||
# jede Zeile bekommt den Dateinamen dazu; mit Block zeilenweise (für streamed)
|
||||
def read(&block)
|
||||
rows = Dir.glob(File.join(@dir, @pattern)).sort.flat_map do |file|
|
||||
DataPipeline::CsvStorage.new(file:, **@csv_options).read.map { |row| row.merge('datei' => File.basename(file, '.*')) }
|
||||
end
|
||||
return rows unless block
|
||||
|
||||
rows.each(&block)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
DataPipeline.register :verzeichnis, VerzeichnisStorage
|
||||
|
||||
DataPipeline.new do
|
||||
read container(:verzeichnis, dir: "#{DATA}/filialen")
|
||||
map { |row| row.merge('menge' => row['menge'].to_i) }
|
||||
write container(:csv, file: "#{out}/alle_filialen.csv", columns: %w[datei artikel menge])
|
||||
end.run
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# Kundenliste aus CSV prüfen, mit Ländern anreichern, nach SQLite und Excel schreiben
|
||||
#
|
||||
# bin/etl run examples/kunden_import.rb [--out=tmp] [--dry-run]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
DATA = File.expand_path('data', __dir__)
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
DataPipeline.new do
|
||||
version 'kunden_import 1.0'
|
||||
|
||||
# Nachschlagetabelle, einmal geladen: { 'DE' => { 'code' => 'DE', 'name' => 'Deutschland', 'eu' => true }, ... }
|
||||
laender = lookup container(:json, file: "#{DATA}/laender.json"), key: 'code', unique: true
|
||||
|
||||
read container(:csv, file: "#{DATA}/kunden.csv", csv: { col_sep: ';' })
|
||||
|
||||
# fehlerhafte Zeilen mit Grund in eine eigene Datei, der Lauf geht weiter
|
||||
on_error :collect, to: container(:csv, file: "#{out}/kunden_fehler.csv")
|
||||
|
||||
# 1. prüfen
|
||||
validate 'kundennr' => { required: true, format: /\A\d{6}\z/ },
|
||||
'name' => { required: true, max: 40 },
|
||||
'umsatz' => { type: :decimal }
|
||||
validate { |row| "Land #{row['land']} unbekannt" unless laender.key?(row['land']) }
|
||||
|
||||
# 2. nur aktive Kunden
|
||||
filter { |row| row['status'] == 'aktiv' }
|
||||
|
||||
# 3. anreichern und umbauen
|
||||
map do |row|
|
||||
land = laender.fetch(row['land'])
|
||||
{
|
||||
'kundennr' => row['kundennr'],
|
||||
'name' => row['name'].strip,
|
||||
'land' => land['name'],
|
||||
'eu' => land['eu'] ? 'ja' : 'nein',
|
||||
'umsatz' => row['umsatz'].to_f.round(2)
|
||||
}
|
||||
end
|
||||
|
||||
# 4. schreiben: alle Kunden nach SQLite, EU-Kunden zusätzlich als Excel-Liste
|
||||
write container(:sqlite, database: "#{out}/kunden.sqlite3", table: 'kunden', create: true, truncate: true)
|
||||
write container(:excel, file: "#{out}/eu_kunden.xlsx", sheet: 'EU-Kunden'), only: ->(row) { row['eu'] == 'ja' }
|
||||
|
||||
on_finish { |stats| puts "kunden_import: #{stats}" }
|
||||
end.run
|
||||
@@ -0,0 +1,50 @@
|
||||
#
|
||||
# Rechnungen aus verschachteltem JSON in eine Datei mit Satzarten (Datei-, Kopf-, Positions-, Schlusssatz)
|
||||
#
|
||||
# bin/etl run examples/rechnungen_satzarten.rb [--out=tmp]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
DATA = File.expand_path('data', __dir__)
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
# Feld: [Name, Typ, Länge, Pflicht]; Typ AN Text, N Zahl (11.2 = 9 Vor-, 2 Nachkommastellen), DAT YYYYMMDD
|
||||
LAYOUT = DataPipeline::Layout.new do
|
||||
record 'DATEI', [['SATYP', 'AN', 5, true], ['ERSTELLT', 'DAT', 8, true], ['ANZAHL', 'N', 6, true]]
|
||||
record 'KOPF', [['SATYP', 'AN', 5, true], ['LFDNR', 'N', 6, true], ['BELNR', 'AN', 10, true],
|
||||
['DATUM', 'DAT', 8, true], ['KUNNR', 'AN', 10, true], ['SUMME', 'N', '11.2', true]]
|
||||
record 'POS', [['SATYP', 'AN', 5, true], ['LFDNR', 'N', 6, true], ['BELNR', 'AN', 10, true],
|
||||
['POSNR', 'N', 3, true], ['TEXT', 'AN', 30], ['BETRAG', 'N', '11.2', true]]
|
||||
record 'ENDE', [['SATYP', 'AN', 5, true], ['ANZAHL', 'N', 6, true]]
|
||||
end
|
||||
|
||||
DataPipeline.new do
|
||||
read container(:json, file: "#{DATA}/rechnungen.json")
|
||||
|
||||
# 1. eine Rechnung -> Kopfsatz + n Positionssätze
|
||||
map do |rechnung|
|
||||
positionen = rechnung['positionen'].each_with_index.map do |pos, i|
|
||||
{ 'satyp' => 'POS', 'belnr' => rechnung['nr'], 'posnr' => format('%03d', i + 1),
|
||||
'text' => pos['text'], 'betrag' => format('%.2f', pos['menge'] * pos['preis']) }
|
||||
end
|
||||
kopf = { 'satyp' => 'KOPF', 'belnr' => rechnung['nr'], 'datum' => rechnung['datum'].delete('-'),
|
||||
'kunnr' => rechnung['kunde'], 'summe' => format('%.2f', positionen.sum { |p| p['betrag'].to_f }) }
|
||||
[kopf, *positionen]
|
||||
end
|
||||
|
||||
# 2. laufende Nummer über alle Kopf- und Positionssätze
|
||||
map { |row, ctx| row.merge('lfdnr' => format('%06d', ctx.index + 1)) }
|
||||
|
||||
# 3. gegen das Layout prüfen: zu langer Text wird gemeldet, die Zeile bleibt
|
||||
validate LAYOUT, warn: true
|
||||
|
||||
# Datei- und Schlusssatz kennen die Anzahl der Datensätze
|
||||
header { |ctx| { 'satyp' => 'DATEI', 'erstellt' => Date.today.strftime('%Y%m%d'), 'anzahl' => format('%06d', ctx.written) } }
|
||||
trailer { |ctx| { 'satyp' => 'ENDE', 'anzahl' => format('%06d', ctx.written) } }
|
||||
|
||||
# Felder in der Reihenfolge des Layouts; Positionen zusätzlich als NDJSON
|
||||
write container(:csv, file: "#{out}/rechnungen.txt", layout: LAYOUT, csv: { headers: false, col_sep: '|' })
|
||||
write container(:json, file: "#{out}/positionen.ndjson"), only: ->(row) { row['satyp'] == 'POS' }
|
||||
end.run
|
||||
@@ -0,0 +1,47 @@
|
||||
#
|
||||
# Kunden seitenweise aus einer REST-API lesen, mit Ländern aus einem zweiten Endpunkt anreichern,
|
||||
# nach CSV und JSON schreiben
|
||||
#
|
||||
# ruby examples/demo_api.rb & # Demo-API starten
|
||||
# bin/etl run examples/rest_lesen.rb [--url=http://127.0.0.1:4567] [--out=tmp]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
API = DataPipeline.params.fetch(:url, 'http://127.0.0.1:4567')
|
||||
AUTH = "Bearer #{ENV.fetch('DEMO_API_TOKEN', 'demo-token')}"
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
DataPipeline.new do
|
||||
version 'rest_lesen 1.0'
|
||||
|
||||
# GET /api/kunden?page=1&per_page=3, dann page=2, ... bis eine Seite leer ist
|
||||
# die Zeilen stehen in der Antwort unter "data"; 429/5xx und Netzfehler werden bis zu 3-mal wiederholt
|
||||
kunden = container :rest, url: "#{API}/api/kunden", auth: AUTH,
|
||||
root: 'data', page_param: 'page', params: { 'per_page' => 3 },
|
||||
retries: 3, retry_wait: 0.2
|
||||
|
||||
# derselbe Server, anderer Endpunkt: query hängt den Pfad an die url an (GET /api/laender)
|
||||
stamm = container :rest, url: "#{API}/api", auth: AUTH
|
||||
laender = lookup stamm, query: 'laender', key: 'code', unique: true
|
||||
|
||||
read kunden
|
||||
|
||||
on_error :collect, to: container(:csv, file: "#{out}/api_fehler.csv")
|
||||
validate 'id' => { required: true }, 'umsatz' => { type: :decimal }
|
||||
|
||||
map do |kunde|
|
||||
{
|
||||
'id' => kunde['id'],
|
||||
'name' => kunde['name'],
|
||||
'land' => laender.dig(kunde['land'], 'name') || kunde['land'],
|
||||
'umsatz' => kunde['umsatz'].to_f
|
||||
}
|
||||
end
|
||||
|
||||
write container(:csv, file: "#{out}/api_kunden.csv", columns: %w[id name land umsatz])
|
||||
write container(:json, file: "#{out}/api_kunden.json", pretty: true)
|
||||
|
||||
on_finish { |stats| puts "rest_lesen: #{stats}" }
|
||||
end.run
|
||||
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# Kunden aus CSV prüfen und in Paketen per POST an eine REST-API senden
|
||||
#
|
||||
# ruby examples/demo_api.rb &
|
||||
# bin/etl run examples/rest_schreiben.rb [--url=http://127.0.0.1:4567] [--dry-run]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
API = DataPipeline.params.fetch(:url, 'http://127.0.0.1:4567')
|
||||
AUTH = "Bearer #{ENV.fetch('DEMO_API_TOKEN', 'demo-token')}"
|
||||
DATA = File.expand_path('data', __dir__)
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
DataPipeline.new do
|
||||
version 'rest_schreiben 1.0'
|
||||
|
||||
read container(:csv, file: "#{DATA}/kunden.csv", csv: { col_sep: ';' })
|
||||
|
||||
# vor dem Senden prüfen: was die API ablehnen würde, landet hier in der Fehlerdatei
|
||||
on_error :collect, to: container(:csv, file: "#{out}/import_fehler.csv")
|
||||
validate 'kundennr' => { required: true, format: /\A\d{6}\z/ }, 'name' => { required: true }
|
||||
|
||||
filter { |row| row['status'] == 'aktiv' }
|
||||
map do |row|
|
||||
{ 'kundennr' => row['kundennr'], 'name' => row['name'].strip, 'land' => row['land'], 'umsatz' => row['umsatz'].to_f }
|
||||
end
|
||||
|
||||
# POST /api/import mit je 2 Kunden als JSON-Array; im Dry-Run wird nichts gesendet
|
||||
write container(:rest, url: "#{API}/api/import", auth: AUTH, batch_size: 2)
|
||||
|
||||
on_finish do |stats|
|
||||
puts stats.dry_run ? 'Dry-Run: nichts gesendet' : "gesendet: #{stats.written} Kunden in #{(stats.written / 2.0).ceil} Paketen"
|
||||
end
|
||||
end.run
|
||||
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# Von API zu API: neue Bestellungen über Folge-Links lesen und einzeln per PUT archivieren
|
||||
# Ein zweiter Lauf ohne neue Bestellungen sendet nichts.
|
||||
#
|
||||
# ruby examples/demo_api.rb &
|
||||
# bin/etl run examples/rest_zu_rest.rb [--url=http://127.0.0.1:4567] [--out=tmp]
|
||||
#
|
||||
require 'fileutils'
|
||||
require 'datapipeline'
|
||||
|
||||
API = DataPipeline.params.fetch(:url, 'http://127.0.0.1:4567')
|
||||
AUTH = "Bearer #{ENV.fetch('DEMO_API_TOKEN', 'demo-token')}"
|
||||
out = DataPipeline.params.fetch(:out, 'tmp')
|
||||
FileUtils.mkdir_p(out)
|
||||
|
||||
DataPipeline.new do
|
||||
streamed
|
||||
|
||||
# höchste Bestellnummer des letzten erfolgreichen Laufs
|
||||
stand = watermark :bestellungen, column: 'id', file: "#{out}/.state.json", initial: 0
|
||||
|
||||
# die Antwort nennt die nächste Seite selbst: {"items": [...], "next": "/api/bestellungen?cursor=2"}
|
||||
read container(:rest, url: "#{API}/api/bestellungen", auth: AUTH, root: 'items',
|
||||
next: ->(body) { body['next'] && "#{API}#{body['next']}" })
|
||||
|
||||
filter { |row| row['id'] > stand.value.to_i }
|
||||
map { |row| row.merge('archiviert' => Date.today.iso8601) }
|
||||
|
||||
# eine url je Bestellung: PUT /api/archiv/<id>
|
||||
write container(:rest, url: "#{API}/api/archiv", auth: AUTH, write_method: :put,
|
||||
write_url: ->(row) { "#{API}/api/archiv/#{row['id']}" })
|
||||
|
||||
on_finish { |stats| puts "archiviert: #{stats.written}, Stand jetzt: #{stand.current}" }
|
||||
end.run
|
||||
Reference in New Issue
Block a user