Kituraのチュートリアル(ToDoBackend)で気になったことを調べた
はじめに
こんにちは。toosaaです。
前回、Kituraのチュートリアルの一つであるToDoBackendを動かしました。今回は、その中で気になった点について調べます。
Codable プロトコル
Swift4から追加されたプロトコルで、これを使うとjsonとの変換を勝手にやってくれます。すごく便利です参考。 今回のチュートリアルでは、Web APIとしてjsonを返すため利用しているのだと思います。そして、このcodableを使うためkituraのversionは swift4なのだと思います。
Equatable プロトコル
ToDoモデルはCodableプロトコル以外にEquatableプロトコルにも準拠しています。これはKituraとして必要というわけではなく、サンプルのupdateHandler()やdeleteHandler()でArrayの中からオブジェクトを見つけるのに必要だから利用しているようです。let idPosition = todoStore.index(of: idMatch)
ここの部分です。
handlerの指定の仕方
チュートリアルだとApplication.swift内にhandlerのメソッドを実装してますが、実運用するにあたってはモデルに紐づけて実装したいです。これは単に、モデルclassにstaticメソッドで同じメソッドを実装すればいいです。とりあえず、データを格納するarray等もstaticにしてしまえば動くものはできます。実際はDBと接続するでしょう。
// Model.swift extension ToDo{ static var todoStore = [ToDo]() static var nextId :Int = 0 static let workerQueue = DispatchQueue(label: "worker") static func storeHandler(todo: ToDo, completion: (ToDo?, RequestError?) -> Void ) { var todo = todo if todo.completed == nil { todo.completed = false } todo.id = nextId todo.url = "http://localhost:8080/\(nextId)" nextId += 1 execute { todoStore.append(todo) } completion(todo, nil) } static func deleteAllHandler(completion: (RequestError?) -> Void ) { execute { todoStore = [ToDo]() } completion(nil) } static func getAllHandler(completion: ([ToDo]?, RequestError?) -> Void ) { completion(todoStore, nil) } static func getOneHandler(id: Int, completion: (ToDo?, RequestError?) -> Void ) { completion(todoStore.first(where: {$0.id == id }), nil) } static func updateHandler(id: Int, new: ToDo, completion: (ToDo?, RequestError?) -> Void ) { guard let idMatch = todoStore.first(where: { $0.id == id }), let idPosition = todoStore.index(of: idMatch) else { return } var current = todoStore[idPosition] current.user = new.user ?? current.user current.order = new.order ?? current.order current.title = new.title ?? current.title current.completed = new.completed ?? current.completed execute { todoStore[idPosition] = current } completion(todoStore[idPosition], nil) } static func deleteOneHandler(id: Int, completion: (RequestError?) -> Void ) { guard let idMatch = todoStore.first(where: { $0.id == id }), let idPosition = todoStore.index(of: idMatch) else { return } execute { todoStore.remove(at: idPosition) } completion(nil) } static func execute(_ block: (() -> Void)) { workerQueue.sync { block() } } }
// Application.swift router.get("/", handler: ToDo.getAllHandler) router.get("/", handler: ToDo.getOneHandler) router.post("/", handler: ToDo.storeHandler) router.delete("/", handler: ToDo.deleteAllHandler) router.delete("/", handler: ToDo.deleteOneHandler) router.patch("/", handler: ToDo.updateHandler)
終わりに
次は、Bluemix上でこのアプリを動作させる方法について書こうかと思います。
Kituraのチュートリアル(ToDoBackend)を試してみた
はじめに
こんにちは。toosaaです。
前回IBM製のフレームワークKituraのGetting Startedに従ってサーバーサイドのHello, World!を行いました。今回は、Getting Startedに続いてチュートリアルとして用意されているToDoBackendを試してみます。そして次回、その内容の一部を調査しようと思います。
動作環境
- macOS High Sierra ver 10.13.3
- Xcode ver 9.2
- Homebrew 1.5.2
事前準備
ToDoBbackendに対して、todo-backend-js-specによるテストをグリーンにしていくことで開発していきます。テスト駆動ですね。そのため、ToDoBackendとtodo-backend-js-specの二つのプロジェクトを用意します。
$ git clone http://github.com/IBM/ToDoBackend
$ git clone http://github.com/TodoBackend/todo-backend-js-spec
試しにテストを実行してみる
$ cd path/to//todo-backend-js-spec
$open index.html
- 起動したブラウザのページのtest target rootに
http://localhost:8080
をセット - run testsをクリック
これでテストを実行できます。おそらく、最初の行にthe api root responds to a GET (i.e. the server is up and accessible, CORS headers are set up)
とエラーが出てると思います。サーバーを起動してないのだからエラー出て当然です。
ToDoBackendのサーバーを初期化する
$ cd path/to/ToDoBackend
$ mkdir ToDoServer
$ cd ToDoServer
$ kitura init
open ToDoServer.xcodeproj
- ビルドスキームをToDoServerに変更(デフォルトではToDoServer-Packageになっているはず)
- Xcode左上の停止ボタン(四角いやつ)の右
- ⌘-Rで実行
- 再度テストを実行する
おそらくまたthe api root responds to a GET (i.e. the server is up and accessible, CORS headers are set up)
とエラーが出てると思います。 Cross Origin Resource Sharing (CORS) が有効になっていないからです。
Cross Origin Resource Sharing (CORS) を有効化する
- CORSライブラリをPackage.swiftに追加する
- (注)チュートリアルページではdepenciesの方にConfigurationとかあり、エラーになった
// Package.swift let package = Package( name: "ToDoServer", dependencies: [ .package(url: "https://github.com/IBM-Swift/Kitura.git", .upToNextMinor(from: "2.0.0")), .package(url: "https://github.com/IBM-Swift/HeliumLogger.git", .upToNextMinor(from: "1.7.1")), .package(url: "https://github.com/IBM-Swift/CloudEnvironment.git", from: "6.0.0"), .package(url: "https://github.com/RuntimeTools/SwiftMetrics.git", from: "2.0.0"), .package(url: "https://github.com/IBM-Swift/Health.git", from: "0.0.0"), //ここ追加 .package(url: "https://github.com/IBM-Swift/Kitura-CORS", .upToNextMinor(from: "2.0.0")), ], targets: [ .target(name: "ToDoServer", dependencies: [ .target(name: "Application"), "Kitura" , "HeliumLogger"]), //ここにKituraCORS追加 .target(name: "Application", dependencies: [ "Kitura", "KituraCORS", "CloudEnvironment", "Health" , "SwiftMetrics", ]), .testTarget(name: "ApplicationTests" , dependencies: [.target(name: "Application"), "Kitura","HeliumLogger" ]) ] )
- packageをインストールするためにregenerateする
- Xcodeを閉じる
$ cd path/to/ToDoBackend/ToDoServer
$ swift package generate-xcodeproj
$ open TodoServer.xcodeproj
- CORSライブラリを使う
- Sources/Application/Application.swiftを開く
import KituraCORS
を追記- postInit()の中に下記のコードを追加
let options = Options(allowedOrigin: .all) let cors = CORS(options: options) router.all("/*", middleware: cors)
この状態で再度テストを行うと、the api root responds to a POST with the todo which was posted to it
に変わっているはずです。(注)もし変更なかったら、テスト用のページを一旦戻りrun testsしてみると変わるかと思います。このテスト結果はPostリクエストの処理を実装してないためです。
Postリクエストの処理を実装する
- Modelを生成する
- XcodeのProjectNavigator上のApplicationフォルダに右クリック
- New File...を選択する
- 名前をModels.swiftにする
- TargetsをApplicationだけにする(おそらくToDoServerPackageDescriptionにチェックがついている)。
- 私の環境だと最初はTargetsを選択する領域が出てなかったのですが、何かのボタンを押したら出たと記憶しています
- Create
- 下記のコードを追加
public struct ToDo : Codable, Equatable { public var id: Int? public var user: String? public var title: String? public var order: Int? public var completed: Bool? public var url: String? public static func ==(lhs: ToDo, rhs: ToDo) -> Bool { return (lhs.title == rhs.title) && (lhs.user == rhs.user) && (lhs.order == rhs.order) && (lhs.completed == rhs.completed) && (lhs.url == rhs.url) && (lhs.id == rhs.id) } }
ToDoアイテムを格納するArrayを作る(このチュートリアルではDBは使わない)
- Sources/Application/Application.swiftを開く
let cloudEnv = CloundEnv()
の下に下記のコードを追加するprivate var todoStore = [ToDo]() private var nextId :Int = 0 private let workerQueue = DispatchQueue(label: "worker")
App Class内に下記メソッドを追加
func execute(_ block: (() -> Void)) { workerQueue.sync { block() } }
/
へのpostリクエストに対応するhandlerを登録する- postInit()に下記コードを追加する
router.post("/", handler: storeHandler)
- App Classに下記のメソッドを追加する
func storeHandler(todo: ToDo, completion: (ToDo?, RequestError?) -> Void ) { var todo = todo if todo.completed == nil { todo.completed = false } todo.id = nextId todo.url = "http://localhost:8080/\(nextId)" nextId += 1 execute { todoStore.append(todo) } completion(todo, nil) }
DELETE, GET, PATCHの実装もする
基本は、postと同じ感じなので割愛
終わりに
今回は、Kituraのチュートリアルの一つToDoBackendを試してみました。次回は、この中で調査したものについて書きます。
Kituraを使ってサーバーサイドSwiftを試してみた
はじめに
すごく久しぶりに投稿します。今後は、友人たちと投稿しようと思います。
今回は、Swiftを使ってサーバーサイドを書いてみたいと思い、IBM製フレームワークのKituraを使ってみた件について、toosaaがお話しします。
内容としては、Getting Startedを動かしてみるまでと、ちょっとした調査となります。
動作環境
- macOS High Sierra ver 10.13.3
- Xcode ver 9.2
- Homebrew 1.5.2
Getting Started
Getting Startedの手順に従います。2017年2月3日現在、同じことを書いているだけです。
事前準備
- Xcode9 のダウンロードとインストール
- Command Line Toolのインストール
$ xcode-select --install
- Homebrewのインストール
Kitura コマンドラインのインストール
$ brew tap ibm-swift/kitura
$ brew tap ibm-swift/kitura
プロジェクトの初期化
$ HelloKitura
$ cd HelloKitura
$ kitura init
- ここにあるように色々なファイルが作られます
- .gitignoreとかもあります
- この記事書いている時に、このページにCRUD projectやmodel generatorというのがあるのに気づきました。あとで読みます。
- ここにあるように色々なファイルが作られます
Hello, World!
$ open HelloKitura.xcodeproj
Sources/Application/Application.swift
のpostInit()
メソッドの中に下記のコードを挿入
注) 私の環境ではimport Kiture
のとこにNo such module 'Kitura'
とエラー出ましたが普通にビルドできます。
// Handle HTTP GET requests to / router.get("/") { request, response, next in response.send("Hello, World!") next() }
- ビルドスキームをHelloKituraに変更(デフォルトではHelloKitura-Packageになっているはず)
- Xcode左上の停止ボタン(四角いやつ)の右
- ⌘-Rで実行
- http://localhost:8080 にアクセス
これだけです。
調査
Hello, World!で書いたコードについて調べてみます。
routeの指定
routerはRouter Classのインスタンスで、getのオーバーロードをいくつか持ってます。そのメソッドの宣言を引っ張って来ると下記の通りです。参考
//RouterHTTPVerbs_generated.swift public func get(_ path: String?=nil, handler: RouterHandler...) -> Router public func get(_ path: String?=nil, handler: [RouterHandler]) -> Router public func get(_ path: String?=nil, allowPartialMatch: Bool = true, middleware: RouterMiddleware...) -> Router public func get(_ path: String?=nil, allowPartialMatch: Bool = true, middleware: [RouterMiddleware]) -> Router
今回使っているのは、これらの一つですが、下記のメソッドもあります。チュートリアルで使われています。
//CodableRouter.swift public func get<O: Codable>(_ route: String, handler: @escaping CodableArrayClosure<O>) public func get<O: Codable>(_ route: String, handler: @escaping SimpleCodableClosure<O>) public func get<Id: Identifier, O: Codable>(_ route: String, handler: @escaping IdentifierSimpleCodableClosure<Id, O>) public func get<Q: QueryParams, O: Codable>(_ route: String, handler: @escaping (Q, @escaping CodableArrayResultClosure<O>) -> Void)
上のget()は、クロージャーを使っていますが、@escapingがついていない&返り値があるので同期処理なのだろうと思います。反対に下のは、非同期処理で使うのだろうと思います。あとCodableなのでjsonのシリアライズでシリアライズ。
どちらにせよ、第一引数の文字列がurlに関るはずです。名前がrouteとpathで違うのはわかりませんが。
http responseを返す
Hello, Worldではresponse.send("Hello, World!")
でhttp responseを返していると予想できます。このresponseはRouterResponse classのインスタンスです。このclassのsend()は下記がとなっています。参考
public func send(_ str: String) -> RouterResponse public func send(data: Data) -> RouterResponse public func send(fileName: String) throws -> RouterResponse public func send(json: [Any]) throws -> RouterResponse public func send(json: [String: Any]) throws -> RouterResponse public func send(status: HTTPStatusCode) -> RouterResponse public func send<T : Encodable>(_ obj: T) throws -> RouterResponse public func send<T : Encodable>(json: T) throws -> RouterResponse public func send<T : Encodable>(jsonp: T, callbackParameter: String = "callback") throws -> RouterResponse
今回は一番最初のstringを返しています。静的なhtmlならsend(fileName: String), web apiならjsonのを使えば良さそうです。
next?
RouterHandlerの第三引数で、@escaping () -> Void
というクロージャーです。ドキュメントでは下記のように書かれています。ちょっと、コードを読んでみましたが、よくわかっていません(・_・;)
The closure to invoke to cause the router to inspect the path in the list of paths.
終わりに
今回は、KituraのGetting Startedと、その中で出てきたコードの簡単な調査を話しました。次回は、チュートリアルの内容に触れようと思います。
youtube-ios-player-helperで再生できないことがある
はじめに
youtube の動画でたまに再生されないものがあったので、それについてのお話
現象
youtube-ios-player-helperを使ってyoutubeの動画再生していました。 大半の動画は再生されるのですが、一部の動画(ex. ID: A_bdeYlpasE)で再生されませんでした。 再生されないというのも、押した反応はあるのですが、その後何も起きないという感じでした(下図参照)。
また、全デリゲートメソッドを監視してみましたが特にエラーが出たりもしませんでした。
原因と対処法
何回もやっていたら偶然か違う挙動をしました。
これを元にライブラリのgithubを調べてみたら、issueが幾つか上がってます#197。 対応としてはパラメーターにoriginを追加すれば良いようです。
self.playerView?.load(withVideoId: "A_bdeYlpasE", playerVars: ["playsinline":1, "origin" : "https://www.youtube.com"]
Mac Book Pro頼んだら決済が通ってなかった
はじめに
先月とうとう発表された新Mac Book Proを意気揚々と予約して今か今かと待ちわびてたんですが
ふとApple Storeのご注文一覧のページにステータスの確認をしに行ったら
お支払い確認中
となってましたorz
状況確認
- 発表された日にMac Book Proを予約
- クレカで12分割払い
- Appleの右上のご注文からご注文一覧見に行った
- なんか赤い文字でエラーっぽい
- クレカ情報入れ直したり別のでやったけどダメ
- メール見に行ったけど何も来てなかった
お支払い確認中
お支払いの確認または承認の問い合わせ中です。この時点では正式なご注文として成立しておりません。ご注文から14日以内(後払いは30日以内)にお支払いまたはお手続きをお願いします。期日までにお支払いの確認または承認が取れない場合は、ご注文は自動的にキャンセルとなります。
何かしないと自動でキャンセルになってしまうので自分から動きましょう
対応
ご注文一覧のページでは何もできなかったのでAppleのサポートに電話しました。 0120-993-993
自動案内でまず4を押し,ご注文一覧ページにあるWから始まる注文番号のWを除いた数字を入力すると担当者につながります。
状況を説明したらクレカの上限とかで決済できなかったのでは?と確認されたがWebで確認した感じ上限は残っていましたし、複数のクレカでダメだったのでそう伝えました。 すると、こういう場合、分割払いにしているのを1割に変更すると通る人が多いと返答がきました。分割から少ない分割にするということはできず、分割から1割に変更するということしかできないそうなので、1割に変更してもらいました。この結果きちんと決済が通りました。よかったε-(´∀`*)ホッ
Reporterを使ってiTunes ConnectのSales and Trendsを自動で取得する - 解析編
はじめに
今回はReporterを使ってiTunes ConnectのSales and Trendsを自動で取得する - 設計編におけるdaily reportの"iTunes Connectから取得"と"インストール数の取得"のサンプルを実装しようと思います。最終的な実装とはちょっと違いますが基本は同じです。
環境
環境 | version |
---|---|
OS | OS X EL Capitan 10.11.6 |
ruby | 2.3.1(rbenv) |
実装方針
- シェルコマンドを実行してreporterを実行
- Zlib::GzipReaderを使ってgzファイルを解凍
- ターゲットとしているアプリかつProduct typeのunitsをかき集める
シェルコマンドを実行してreporterを実行
reporter自体はjavaで実行なのでrubyからシェルコマンドを実行する仕組みを実装します。Rubyで外部コマンドを実行して結果を受け取る方法あれこれを見るといろいろあるみたいですけど一番簡単そうなバッククォートで良さそうです。
date = "20161101" report_xml = `java -jar Reporter.jar p=Reporter.properties m=Robot.XML Sales.getReport <vendor id>, Sales, Summary, Daily, #{date}`
Zlib::GzipReaderを使ってgzファイルを解凍
サンプル見るのが早いかと思います。
require 'zlib' date = "20161101" file_name = "S_D_<vendor id>_#{date}.txt.gz" Zlib::GzipReader.open(file_name){|gz| # 最初のlineはムシ gz.gets while s = gz.gets p s }
ターゲットとしているアプリかつProduct typeのunitsをかき集める
今回は簡略化のため一つのアプリに限定してます。
ここで問題となるのが、取得できるデータが各アプリのインストール毎という形式ではなく、各アプリのProduct Typeや地域毎という感じで別れてしまっていることです。今回は、ターゲットとしたアプリの全地域でのインストール数を取得するという方針でいきます。
require 'zlib' # http://help.apple.com/itc/appssalesandtrends/#/itc0c699d615 # アップデートとかを集計しないため TargetProductIds = ["1", "1F", "1T"].freeze TargetBundleId = "hogehoge".freeze date = "20161101" file_name = "S_D_<vendor id>_#{date}.txt.gz" app_unitis = 0 Zlib::GzipReader.open(file_name){|gz| # 最初のlineはムシ gz.gets while s = gz.gets # tsvでタグ区切りなので rows = s.split("\t") # 正確にはSKUだったけどほとんどの場合bundle_idと同じにするだろうからこのままでorz bundle_id = rows[2] units = rows[7].to_i product_type_id = rows[6] if !TargetProductIds.include?(product_type_id) next end if bundle_id == TargetBundleId app_units += units end end } p app_units
コード全部
require 'rexml/document' require 'zlib' # http://help.apple.com/itc/appssalesandtrends/#/itc0c699d615 # アップデートとかを集計しないため TargetProductIds = ["1", "1F", "1T"].freeze TargetBundleId = "hogehoge".freeze VendorId = 1111111.freeze date = "20161101" report_xml = `java -jar Reporter.jar p=Reporter.properties m=Robot.XML Sales.getReport #{VendorId}, Sales, Summary, Daily, #{date}` report_result = REXML::Document.new(get_report_xml) unless report_result.elements['Error/Code'].nil? p get_report_result.elements['Error/Code'].text p get_report_result.elements['Error/Message'].text # TODO: きちんと例外処理 raise "Reporterのダウンロードに失敗しました" end file_name = "S_D_#{VendorId}_#{date}.txt.gz" app_unitis = 0 Zlib::GzipReader.open(file_name){|gz| # 最初のlineはムシ gz.gets while s = gz.gets # tsvでタグ区切りなので rows = s.split("\t") # 正確にはSKUだったけどほとんどの場合bundle_idと同じにするだろうからこのままでorz bundle_id = rows[2] units = rows[7].to_i product_type_id = rows[6] if !TargetProductIds.include?(product_type_id) next end if bundle_id == TargetBundleId app_units += units end end } p app_units
SpreadsheetをRubyで操作する
はじめに
Reporterを使ってiTunes ConnectのSales and Trendsを自動で取得する - 設計編で実装指針としては書いていたGoogle SpreadsheetとRubyの連携について書きます。
なお環境は下記の通りです。
環境 | version |
---|---|
OS | OS X EL Capitan 10.11.6 |
ruby | 2.3.1(rbenv) |
ライブラリ(gem)
google_driveというgemを利用します。参考ページは下記の通りです。
- Authorization(service account)
- Example to read/write spreadsheets
- API Documents
- RubyでOAuth認証なしにGoogleSpreadSheetのデータを取得
実装
事前準備
誰でも好き勝手にSpreadsheetを操作できるようにしてしまうのはまずいので認証が必要です。そのための準備をします。
サービスアカウントの作成
On behalf of no existing users (service account)こちらの手順に従ってもらえれば、認証用のjsonファイルがダウンロードされます。
スプレッドシートの共有設定
先ほど作成したサービスアカウントのメールアドレス(jsonに書いてあります)を操作対象のSpreadsheetで共有します。
bundlerのインストール
gemの管理をbundlerを利用したいのでbundlerをinstallします。
$ cd path/to/directory
$ rbenv exec gem install bundler
google_driveのインストール
Gemfileというファイルを作成して下記を書き込みます。
source 'https://rubygems.org' gem 'google_drive'
そしてgemをインストールします。
$ bundle install --path vendor/bundle
Example実装
Example to read/write spreadsheetsを実行してみます。
準備
- サービスアカウントを作成した時にダウンロードしたjsonをrubyプログラムと同じディレクトリに
config.json
という名前で置きます - 操作対象のスプレッドシートキーを取得します
https://docs.google.com/spreadsheets/d/<スプレッドシートキー>/edit
みたいな感じになってるのでそこから取得できます(ちゃんとどこかに書いてあるとは思いますが...)
サンプルコード
自分はgithubのコードにrequire 'bundler/setup'
を追加しないと動きませんでした。またsessionの作り方もサービスアカウントを使う場合に書き換えています。
require 'bundler/setup' require "google_drive" # Creates a session. This will prompt the credential via command line for the # first time and save it to config.json file for later usages. session = GoogleDrive::Session.from_service_account_key("config.json") # First worksheet of # https://docs.google.com/spreadsheet/ccc?key=pz7XtlQC-PYx-jrVMJErTcg # Or https://docs.google.com/a/someone.com/spreadsheets/d/pz7XtlQC-PYx-jrVMJErTcg/edit?usp=drive_web ws = session.spreadsheet_by_key(<スプレッドシートキー>).worksheets[0] # Gets content of A2 cell. p ws[2, 1] #==> "hoge" # Changes content of cells. # Changes are not sent to the server until you call ws.save(). ws[2, 1] = "foo" ws[2, 2] = "bar" ws.save # Dumps all cells. (1..ws.num_rows).each do |row| (1..ws.num_cols).each do |col| p ws[row, col] end end # Yet another way to do so. p ws.rows #==> [["fuga", ""], ["foo", "bar]] # Reloads the worksheet to get changes by other clients. ws.reload
APIピックアップ
Session作成
該当箇所: session = GoogleDrive::Session.from_service_account_key("config.json")
GoogleDrive/Sessionにあるように認証のやり方でメソッドが変わります。今回はサービスアカウントを利用したためExampleとは違い.from_service_account_key(json_key_path_or_io, scope = DEFAULT_SCOPE) ⇒ Object
を利用しました。
Spreadsheetの取得
該当箇所: session.spreadsheet_by_key(<スプレッドシートキー>)
GoogleDriveとかから見えるスプレッドシートの単位だと思います。キー以外にtitleとurlからも取得できます(参考)。
WorkSheet取得
該当箇所: ws = session.spreadsheet_by_key(<スプレッドシートキー>).worksheets[0]
スプレッドシート内でのシート(画面左下の+ボタンで追加できるもの)のことのようです。サンプルでは0番目のシートを取得していますが、titleやgid(urlを見るとわかります)などから取得することも可能です(参考)。
WorkSheetの操作
セルの値取得
例えば C2の値をとるならば c2 = ws[2, 3]
とやれば取れます。簡単ですね。
セルの値変更
C2に"てくてくテック"と入れる場合は下記のようになります。
ws[2, 3] = "てくてくテック" ws.save
最後の行や列の番号
ws.max_cols
やws.max_rows
で取得できます
行の挿入
#insert_rows(row_num, rows) ⇒ Object
を使います。row_num行目にrows行追加します。
e.g(ドキュメントより)
# Inserts 2 empty rows before row 3. worksheet.insert_rows(3, 2) # Inserts 2 rows with values before row 3. worksheet.insert_rows(3, [["a, "b"], ["c, "d"]])