Ecoer Logo

@typenil

32

Developer

steemit.com/@typenil
VOTING POWER100.00%
DOWNVOTE POWER100.00%
RESOURCE CREDITS100.00%
REPUTATION PROGRESS9.47%
Net Worth
0.012USD
STEEM
0.073STEEM
SBD
0.000SBD
Effective Power
5.007SP
├── Own SP
0.126SP
└── Incoming Deleg
+4.881SP

Detailed Balance

STEEM
balance
0.001STEEM
market_balance
0.000STEEM
savings_balance
0.000STEEM
reward_steem_balance
0.072STEEM
STEEM POWER
Own SP
0.126SP
Delegated Out
0.000SP
Delegation In
4.881SP
Effective Power
5.007SP
Reward SP (pending)
0.132SP
SBD
sbd_balance
0.000SBD
sbd_conversions
0.000SBD
sbd_market_balance
0.000SBD
savings_sbd_balance
0.000SBD
reward_sbd_balance
0.000SBD
{
  "balance": "0.001 STEEM",
  "savings_balance": "0.000 STEEM",
  "reward_steem_balance": "0.072 STEEM",
  "vesting_shares": "204.284588 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "7939.375218 VESTS",
  "sbd_balance": "0.000 SBD",
  "savings_sbd_balance": "0.000 SBD",
  "reward_sbd_balance": "0.000 SBD",
  "conversions": []
}

Account Info

nametypenil
id794214
rank527,359
reputation6141895570
created2018-03-02T19:34:15
recovery_accountsteem
proxyNone
post_count5
comment_count0
lifetime_vote_count0
witnesses_voted_for0
last_post2019-12-12T16:15:03
last_root_post2019-12-12T16:15:03
last_vote_time1970-01-01T00:00:00
proxied_vsf_votes0, 0, 0, 0
can_vote1
voting_power0
delayed_votes0
balance0.001 STEEM
savings_balance0.000 STEEM
sbd_balance0.000 SBD
savings_sbd_balance0.000 SBD
vesting_shares204.284588 VESTS
delegated_vesting_shares0.000000 VESTS
received_vesting_shares7939.375218 VESTS
reward_vesting_balance263.453058 VESTS
vesting_balance0.000 STEEM
vesting_withdraw_rate0.000000 VESTS
next_vesting_withdrawal1969-12-31T23:59:59
withdrawn0
to_withdraw0
withdraw_routes0
savings_withdraw_requests0
last_account_recovery1970-01-01T00:00:00
reset_accountnull
last_owner_update1970-01-01T00:00:00
last_account_update2019-12-12T16:16:00
minedNo
sbd_seconds0
sbd_last_interest_payment1970-01-01T00:00:00
savings_sbd_last_interest_payment1970-01-01T00:00:00
{
  "id": 794214,
  "name": "typenil",
  "owner": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5JBNRJKnW9hTWAhoDXppLA1FTnJMV7agze5ye8gd1Zb4ZGqcFK",
        1
      ]
    ]
  },
  "active": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5YAtprKXbKameJcDhnwxELYvTH953VQRCeU6VuvnvEWeCXnor7",
        1
      ]
    ]
  },
  "posting": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM6YK6wa24fPaeyVcvS5a2EKarRR7b2RwwHvgo6cDwGPd24sxBD5",
        1
      ]
    ]
  },
  "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
  "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Developer\",\"website\":\"https://typenil.com/\",\"name\":\"typenil\",\"location\":\"United States\"}}",
  "posting_json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Developer\",\"website\":\"https://mdub.dev/\",\"name\":\"Matt White\",\"location\":\"United States\"}}",
  "proxy": "",
  "last_owner_update": "1970-01-01T00:00:00",
  "last_account_update": "2019-12-12T16:16:00",
  "created": "2018-03-02T19:34:15",
  "mined": false,
  "recovery_account": "steem",
  "last_account_recovery": "1970-01-01T00:00:00",
  "reset_account": "null",
  "comment_count": 0,
  "lifetime_vote_count": 0,
  "post_count": 5,
  "can_vote": true,
  "voting_manabar": {
    "current_mana": "8143659806",
    "last_update_time": 1779090285
  },
  "downvote_manabar": {
    "current_mana": 2035914951,
    "last_update_time": 1779090285
  },
  "voting_power": 0,
  "balance": "0.001 STEEM",
  "savings_balance": "0.000 STEEM",
  "sbd_balance": "0.000 SBD",
  "sbd_seconds": "0",
  "sbd_seconds_last_update": "1970-01-01T00:00:00",
  "sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_sbd_balance": "0.000 SBD",
  "savings_sbd_seconds": "0",
  "savings_sbd_seconds_last_update": "1970-01-01T00:00:00",
  "savings_sbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_withdraw_requests": 0,
  "reward_sbd_balance": "0.000 SBD",
  "reward_steem_balance": "0.072 STEEM",
  "reward_vesting_balance": "263.453058 VESTS",
  "reward_vesting_steem": "0.132 STEEM",
  "vesting_shares": "204.284588 VESTS",
  "delegated_vesting_shares": "0.000000 VESTS",
  "received_vesting_shares": "7939.375218 VESTS",
  "vesting_withdraw_rate": "0.000000 VESTS",
  "next_vesting_withdrawal": "1969-12-31T23:59:59",
  "withdrawn": 0,
  "to_withdraw": 0,
  "withdraw_routes": 0,
  "curation_rewards": 0,
  "posting_rewards": 204,
  "proxied_vsf_votes": [
    0,
    0,
    0,
    0
  ],
  "witnesses_voted_for": 0,
  "last_post": "2019-12-12T16:15:03",
  "last_root_post": "2019-12-12T16:15:03",
  "last_vote_time": "1970-01-01T00:00:00",
  "post_bandwidth": 0,
  "pending_claimed_accounts": 0,
  "vesting_balance": "0.000 STEEM",
  "reputation": "6141895570",
  "transfer_history": [],
  "market_history": [],
  "post_history": [],
  "vote_history": [],
  "other_history": [],
  "witness_votes": [],
  "tags_usage": [],
  "guest_bloggers": [],
  "rank": 527359
}

Withdraw Routes

IncomingOutgoing
Empty
Empty
{
  "incoming": [],
  "outgoing": []
}
From Date
To Date
steemdelegated 4.881 SP to @typenil
2026/05/18 07:44:45
delegatorsteem
delegateetypenil
vesting shares7939.375218 VESTS
Transaction InfoBlock #106152402/Trx 05408a2f2715d13475a6ba7c6de323726922a439
View Raw JSON Data
{
  "trx_id": "05408a2f2715d13475a6ba7c6de323726922a439",
  "block": 106152402,
  "trx_in_block": 2,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-05-18T07:44:45",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "7939.375218 VESTS"
    }
  ]
}
steemdelegated 3.214 SP to @typenil
2026/05/13 10:12:54
delegatorsteem
delegateetypenil
vesting shares5227.164813 VESTS
Transaction InfoBlock #106012073/Trx cacfeb9c10c3c52afe87f72266e87a32d77961c0
View Raw JSON Data
{
  "trx_id": "cacfeb9c10c3c52afe87f72266e87a32d77961c0",
  "block": 106012073,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-05-13T10:12:54",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "5227.164813 VESTS"
    }
  ]
}
steemdelegated 4.889 SP to @typenil
2026/04/26 06:54:42
delegatorsteem
delegateetypenil
vesting shares7951.890974 VESTS
Transaction InfoBlock #105519851/Trx f7c801aca8e3b48e2273da267f8f178099500cf3
View Raw JSON Data
{
  "trx_id": "f7c801aca8e3b48e2273da267f8f178099500cf3",
  "block": 105519851,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-04-26T06:54:42",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "7951.890974 VESTS"
    }
  ]
}
steemdelegated 3.239 SP to @typenil
2026/01/24 03:55:18
delegatorsteem
delegateetypenil
vesting shares5268.711632 VESTS
Transaction InfoBlock #102875905/Trx 4a6a418449916f6e69d0546bc36137132cc801b1
View Raw JSON Data
{
  "trx_id": "4a6a418449916f6e69d0546bc36137132cc801b1",
  "block": 102875905,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2026-01-24T03:55:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "5268.711632 VESTS"
    }
  ]
}
steemdelegated 3.340 SP to @typenil
2024/12/17 23:03:51
delegatorsteem
delegateetypenil
vesting shares5432.930829 VESTS
Transaction InfoBlock #91322096/Trx 94b4d2be350dd94bd73c8b3cf34a33da0b25832e
View Raw JSON Data
{
  "trx_id": "94b4d2be350dd94bd73c8b3cf34a33da0b25832e",
  "block": 91322096,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2024-12-17T23:03:51",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "5432.930829 VESTS"
    }
  ]
}
steemdelegated 3.444 SP to @typenil
2023/11/14 14:42:15
delegatorsteem
delegateetypenil
vesting shares5602.064361 VESTS
Transaction InfoBlock #79876179/Trx de6ba3260a22108296a759fa64f86c9dfbb08ed1
View Raw JSON Data
{
  "trx_id": "de6ba3260a22108296a759fa64f86c9dfbb08ed1",
  "block": 79876179,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2023-11-14T14:42:15",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "5602.064361 VESTS"
    }
  ]
}
steemdelegated 5.250 SP to @typenil
2023/09/22 12:05:48
delegatorsteem
delegateetypenil
vesting shares8538.973147 VESTS
Transaction InfoBlock #78364903/Trx 4962e991249aebc1232d43922d1caf8095c94e59
View Raw JSON Data
{
  "trx_id": "4962e991249aebc1232d43922d1caf8095c94e59",
  "block": 78364903,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2023-09-22T12:05:48",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "8538.973147 VESTS"
    }
  ]
}
steemdelegated 5.386 SP to @typenil
2022/11/03 19:22:18
delegatorsteem
delegateetypenil
vesting shares8761.024585 VESTS
Transaction InfoBlock #69122420/Trx bc99f3bc97635ba6c09a067e81e4194fe8142dab
View Raw JSON Data
{
  "trx_id": "bc99f3bc97635ba6c09a067e81e4194fe8142dab",
  "block": 69122420,
  "trx_in_block": 6,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2022-11-03T19:22:18",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "8761.024585 VESTS"
    }
  ]
}
steemdelegated 5.521 SP to @typenil
2022/01/18 00:25:42
delegatorsteem
delegateetypenil
vesting shares8981.132186 VESTS
Transaction InfoBlock #60825500/Trx b00241691cbd28dc0313c5c39df38e7fd7f49809
View Raw JSON Data
{
  "trx_id": "b00241691cbd28dc0313c5c39df38e7fd7f49809",
  "block": 60825500,
  "trx_in_block": 78,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2022-01-18T00:25:42",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "8981.132186 VESTS"
    }
  ]
}
steemdelegated 5.635 SP to @typenil
2021/06/14 07:33:03
delegatorsteem
delegateetypenil
vesting shares9165.326474 VESTS
Transaction InfoBlock #54615748/Trx 7fe74409bb5a003d3fb304ded5c17c91afa25ba2
View Raw JSON Data
{
  "trx_id": "7fe74409bb5a003d3fb304ded5c17c91afa25ba2",
  "block": 54615748,
  "trx_in_block": 5,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2021-06-14T07:33:03",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9165.326474 VESTS"
    }
  ]
}
steemdelegated 5.750 SP to @typenil
2020/12/11 17:43:57
delegatorsteem
delegateetypenil
vesting shares9352.748448 VESTS
Transaction InfoBlock #49362965/Trx 50c7b9744208a43dc11852b2eac3fad382924832
View Raw JSON Data
{
  "trx_id": "50c7b9744208a43dc11852b2eac3fad382924832",
  "block": 49362965,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-11T17:43:57",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9352.748448 VESTS"
    }
  ]
}
steemdelegated 1.176 SP to @typenil
2020/12/06 11:19:15
delegatorsteem
delegateetypenil
vesting shares1912.543513 VESTS
Transaction InfoBlock #49214482/Trx f55e4008f26cfb35cf9046421532da6263fa6b42
View Raw JSON Data
{
  "trx_id": "f55e4008f26cfb35cf9046421532da6263fa6b42",
  "block": 49214482,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-06T11:19:15",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "1912.543513 VESTS"
    }
  ]
}
steemdelegated 5.754 SP to @typenil
2020/12/05 21:21:51
delegatorsteem
delegateetypenil
vesting shares9358.956302 VESTS
Transaction InfoBlock #49198050/Trx 9b48612e0b306f4a1e7842d629ad42e40b5ad3e9
View Raw JSON Data
{
  "trx_id": "9b48612e0b306f4a1e7842d629ad42e40b5ad3e9",
  "block": 49198050,
  "trx_in_block": 2,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-12-05T21:21:51",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9358.956302 VESTS"
    }
  ]
}
steemdelegated 1.180 SP to @typenil
2020/11/03 05:18:45
delegatorsteem
delegateetypenil
vesting shares1920.017158 VESTS
Transaction InfoBlock #48273901/Trx 2b350b8e5d16c32d720e5fc5ff9231c9a0e39bd1
View Raw JSON Data
{
  "trx_id": "2b350b8e5d16c32d720e5fc5ff9231c9a0e39bd1",
  "block": 48273901,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-11-03T05:18:45",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "1920.017158 VESTS"
    }
  ]
}
steemdelegated 5.878 SP to @typenil
2020/05/09 12:23:36
delegatorsteem
delegateetypenil
vesting shares9561.761661 VESTS
Transaction InfoBlock #43224830/Trx 942374266539f6fb592c0bc49ae0021fd2af6393
View Raw JSON Data
{
  "trx_id": "942374266539f6fb592c0bc49ae0021fd2af6393",
  "block": 43224830,
  "trx_in_block": 17,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-05-09T12:23:36",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9561.761661 VESTS"
    }
  ]
}
steemdelegated 1.201 SP to @typenil
2020/05/08 16:59:24
delegatorsteem
delegateetypenil
vesting shares1953.311140 VESTS
Transaction InfoBlock #43202096/Trx dc6e7b9d2d9c885511fc69bcbdeaa9c16ad8ee6a
View Raw JSON Data
{
  "trx_id": "dc6e7b9d2d9c885511fc69bcbdeaa9c16ad8ee6a",
  "block": 43202096,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-05-08T16:59:24",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "1953.311140 VESTS"
    }
  ]
}
steemdelegated 5.897 SP to @typenil
2020/03/12 17:05:21
delegatorsteem
delegateetypenil
vesting shares9591.631502 VESTS
Transaction InfoBlock #41592435/Trx ad97d37b5fa2360595a647ea7cbb7801ce57d81b
View Raw JSON Data
{
  "trx_id": "ad97d37b5fa2360595a647ea7cbb7801ce57d81b",
  "block": 41592435,
  "trx_in_block": 23,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-03-12T17:05:21",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9591.631502 VESTS"
    }
  ]
}
2020/03/05 12:16:39
parent authortypenil
parent permlinkhijacking-default-django-through-tables
authorsteemitboard
permlinksteemitboard-notify-typenil-20200305t121638000z
title
bodyCongratulations @typenil! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@typenil/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table> <sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@typenil) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=typenil)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge"><img src="https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmTugCUsoXX762vg1CuHRrpnPbfnjPogp8iCGv7F2kSVuj/image.png"></a></td><td><a href="https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge">Use your witness votes and get the Community Badge</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
Transaction InfoBlock #41385556/Trx 100e237f2c149c936eebc2e84c32ea3ed3d99e56
View Raw JSON Data
{
  "trx_id": "100e237f2c149c936eebc2e84c32ea3ed3d99e56",
  "block": 41385556,
  "trx_in_block": 8,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2020-03-05T12:16:39",
  "op": [
    "comment",
    {
      "parent_author": "typenil",
      "parent_permlink": "hijacking-default-django-through-tables",
      "author": "steemitboard",
      "permlink": "steemitboard-notify-typenil-20200305t121638000z",
      "title": "",
      "body": "Congratulations @typenil! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@typenil/birthday2.png</td><td>Happy Birthday! - You are on the Steem blockchain for 2 years!</td></tr></table>\n\n<sub>_You can view [your badges on your Steem Board](https://steemitboard.com/@typenil) and compare to others on the [Steem Ranking](https://steemitboard.com/ranking/index.php?name=typenil)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge\"><img src=\"https://steemitimages.com/64x128/https://cdn.steemitimages.com/DQmTugCUsoXX762vg1CuHRrpnPbfnjPogp8iCGv7F2kSVuj/image.png\"></a></td><td><a href=\"https://steemit.com/steemitboard/@steemitboard/use-your-witness-votes-and-get-the-community-badge\">Use your witness votes and get the Community Badge</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) to get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}"
    }
  ]
}
typenilreceived 0.072 STEEM, 0.087 SP author reward for @typenil / hijacking-default-django-through-tables
2019/12/19 16:15:03
authortypenil
permlinkhijacking-default-django-through-tables
sbd payout0.000 SBD
steem payout0.072 STEEM
vesting payout141.757006 VESTS
Transaction InfoBlock #39178066/Virtual Operation #20
View Raw JSON Data
{
  "trx_id": "0000000000000000000000000000000000000000",
  "block": 39178066,
  "trx_in_block": 4294967295,
  "op_in_trx": 0,
  "virtual_op": 20,
  "timestamp": "2019-12-19T16:15:03",
  "op": [
    "author_reward",
    {
      "author": "typenil",
      "permlink": "hijacking-default-django-through-tables",
      "sbd_payout": "0.000 SBD",
      "steem_payout": "0.072 STEEM",
      "vesting_payout": "141.757006 VESTS"
    }
  ]
}
2019/12/18 05:48:51
voterknifer
authortypenil
permlinkhijacking-default-django-through-tables
weight10000 (100.00%)
Transaction InfoBlock #39136820/Trx e1c17ca563674e2dce14441d34ea00bc85885967
View Raw JSON Data
{
  "trx_id": "e1c17ca563674e2dce14441d34ea00bc85885967",
  "block": 39136820,
  "trx_in_block": 1,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-18T05:48:51",
  "op": [
    "vote",
    {
      "voter": "knifer",
      "author": "typenil",
      "permlink": "hijacking-default-django-through-tables",
      "weight": 10000
    }
  ]
}
2019/12/18 05:48:45
voteremrebeyler
authortypenil
permlinkhijacking-default-django-through-tables
weight10000 (100.00%)
Transaction InfoBlock #39136818/Trx 87dd509e617886c49a52add68c16e3ad0ebdf638
View Raw JSON Data
{
  "trx_id": "87dd509e617886c49a52add68c16e3ad0ebdf638",
  "block": 39136818,
  "trx_in_block": 10,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-18T05:48:45",
  "op": [
    "vote",
    {
      "voter": "emrebeyler",
      "author": "typenil",
      "permlink": "hijacking-default-django-through-tables",
      "weight": 10000
    }
  ]
}
steemdelegated 18.100 SP to @typenil
2019/12/12 17:42:06
delegatorsteem
delegateetypenil
vesting shares29441.832530 VESTS
Transaction InfoBlock #38978579/Trx deafef3cd71eafc973fa7abd23ae5a13c73da893
View Raw JSON Data
{
  "trx_id": "deafef3cd71eafc973fa7abd23ae5a13c73da893",
  "block": 38978579,
  "trx_in_block": 22,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-12T17:42:06",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "29441.832530 VESTS"
    }
  ]
}
2019/12/12 16:22:39
voterabojasim880
authortypenil
permlinkhijacking-default-django-through-tables
weight10000 (100.00%)
Transaction InfoBlock #38976992/Trx 85072e6f3cb8299f15053294a7b91b32885b4fbd
View Raw JSON Data
{
  "trx_id": "85072e6f3cb8299f15053294a7b91b32885b4fbd",
  "block": 38976992,
  "trx_in_block": 2,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-12T16:22:39",
  "op": [
    "vote",
    {
      "voter": "abojasim880",
      "author": "typenil",
      "permlink": "hijacking-default-django-through-tables",
      "weight": 10000
    }
  ]
}
typenilupdated their account properties
2019/12/12 16:16:00
accounttypenil
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{"profile":{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Developer","website":"https://typenil.com/","name":"typenil","location":"United States"}}
Transaction InfoBlock #38976859/Trx 98f6e77f68eac50b4b7de0af7380f557315f46d9
View Raw JSON Data
{
  "trx_id": "98f6e77f68eac50b4b7de0af7380f557315f46d9",
  "block": 38976859,
  "trx_in_block": 15,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-12T16:16:00",
  "op": [
    "account_update",
    {
      "account": "typenil",
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Developer\",\"website\":\"https://typenil.com/\",\"name\":\"typenil\",\"location\":\"United States\"}}"
    }
  ]
}
2019/12/12 16:15:03
parent author
parent permlinkpython
authortypenil
permlinkhijacking-default-django-through-tables
titleHijacking Default Django 'Through' Tables
bodyA few times in the last year, I've run into the need to add some metadata to a Django many-to-many relationship. By default, there's no explicit model to add fields to, but - if you're working on an active project - you probably have existing data in the default 'through' table that you don't want to lose. So what are you to do if you don't want to have to create a completely new table and migrate the data over? Let's hijack the existing one. The existing models: ``` from django.db import models class ModelA(models.Model): b_models = models.ManyToManyField("app.ModelB", related_name="a_models", blank=True) class Meta: db_table = "app_model_a" class ModelB(models.Model): class Meta: db_table = "app_model_b" ``` ----- Create a model that matches the existing 'through' table exactly - and make sure to specify the existing table name using `Meta.db_table`: ``` from django.db import models class ModelAModelB(models.Model): modela = models.ForeignKey("app.ModelA", on_delete=models.CASCADE) modelb = models.ForeignKey("app.ModelB", on_delete=models.CASCADE) class Meta: db_table = "app_model_a_model_b" ``` ----- Update the many-to-many relationship to use the new model: ``` class ModelA(models.Model): b_models = models.ManyToManyField("app.ModelB", related_name="a_models", through="app.ModelAModelB", blank=True) class Meta: db_table = "app_model_a" ``` ----- Now generate a new migration with `python manage.py makemigrations`; it'll need some editing. The initial migration: ``` import datetime from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('api_app', '69420_whatever_your_last_migration_was'), ] operations = [ migrations.CreateModel( name='ModelAModelB', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], options={ 'db_table': 'app_model_a_model_b', }, ), migrations.AlterField( model_name='modela', name='b_models', field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'), ), migrations.AddField( model_name='modelamodelb', name='modela', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'), ), migrations.AddField( model_name='modelamodelb', name='modelb', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'), ), ] ``` ----- We just need to wrap these operations in `migrations.SeparateDatabaseAndState` to get the models in sync without screwing up the existing database setup. All of the changes above represent state changes: ``` import datetime from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('api_app', '69420_whatever_your_last_migration_was'), ] state_operations = [ migrations.CreateModel( name='ModelAModelB', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ], options={ 'db_table': 'app_model_a_model_b', }, ), migrations.AlterField( model_name='modela', name='b_models', field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'), ), migrations.AddField( model_name='modelamodelb', name='modela', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'), ), migrations.AddField( model_name='modelamodelb', name='modelb', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'), ), ] operations = [ migrations.SeparateDatabaseAndState( state_operations=state_operations, ) ] ``` ----- And that should do it. That migration should successfully tie the existing 'through' table to your new model. Now you can add fields and create additional migrations as normal. ### For my notes - filtering on 'through' fields I want to make one additional note - since it wasn't immediately obvious to me. With Django 2.2, you should be able to filter on the 'through' table fields by using the lowercase name of the 'through' model. You can add something like `state` to the 'through' model: ``` class ModelAModelB(models.Model): modela = models.ForeignKey("app.ModelA", on_delete=models.CASCADE) modelb = models.ForeignKey("app.ModelB", on_delete=models.CASCADE) state = models.CharField( max_length=16, null=True, blank=True, choices=[("good", "good"), ("ungood", "ungood")] ) class Meta: db_table = "app_model_a_model_b" ``` Then filtering on this field looks a lot like this: ``` # querysets ModelA.objects.filter(modelamodelb__state="good") ModelB.objects.filter(modelamodelb__state="ungood") ModelAModelB.objects.filter(state="good") # related manager on instances a_instance = ModelA.objects.first() a_instance.b_models.filter(modelamodelb__state="ungood") b_instance = ModelB.objects.first() b_instance.a_models.filter(modelamodelb__state="good") ``` ----- Now hopefully I can just come back to this post next time I need to do this. ----- *Prefer to catch my posts elsewhere?* Originally posted on my blog: https://typenil.com/hijacking-default-django-through-tables/
json metadata{"tags":["python","django","migrations"],"links":["https://typenil.com/hijacking-default-django-through-tables/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #38976840/Trx 541891bc7a562294316a29232881bac6dab2d060
View Raw JSON Data
{
  "trx_id": "541891bc7a562294316a29232881bac6dab2d060",
  "block": 38976840,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-12-12T16:15:03",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "hijacking-default-django-through-tables",
      "title": "Hijacking Default Django 'Through' Tables",
      "body": "A few times in the last year, I've run into the need to add some metadata to a Django many-to-many relationship. By default, there's no explicit model to add fields to, but - if you're working on an active project - you probably have existing data in the default 'through' table that you don't want to lose. So what are you to do if you don't want to have to create a completely new table and migrate the data over? Let's hijack the existing one.\n\nThe existing models:\n```\nfrom django.db import models\n\nclass ModelA(models.Model):\n    b_models = models.ManyToManyField(\"app.ModelB\", related_name=\"a_models\", blank=True)\n\n    class Meta:\n        db_table = \"app_model_a\"\n\n\nclass ModelB(models.Model):\n    class Meta:\n        db_table = \"app_model_b\"\n```\n-----\n\nCreate a model that matches the existing 'through' table exactly - and make sure to specify the existing table name using `Meta.db_table`:\n\n```\nfrom django.db import models\n\n\nclass ModelAModelB(models.Model):\n\n    modela = models.ForeignKey(\"app.ModelA\", on_delete=models.CASCADE)\n    modelb = models.ForeignKey(\"app.ModelB\", on_delete=models.CASCADE)\n\n    class Meta:\n        db_table = \"app_model_a_model_b\"\n```\n-----\n\nUpdate the many-to-many relationship to use the new model:\n```\n\nclass ModelA(models.Model):\n    b_models = models.ManyToManyField(\"app.ModelB\", related_name=\"a_models\", through=\"app.ModelAModelB\", blank=True)\n\n    class Meta:\n        db_table = \"app_model_a\"\n\n\n```\n-----\n\nNow generate a new migration with `python manage.py makemigrations`; it'll need some editing. The initial migration:\n```\nimport datetime\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('api_app', '69420_whatever_your_last_migration_was'),\n    ]\n\n    operations = [\n        migrations.CreateModel(\n            name='ModelAModelB',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n            ],\n            options={\n                'db_table': 'app_model_a_model_b',\n            },\n        ),\n        migrations.AlterField(\n            model_name='modela',\n            name='b_models',\n            field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'),\n        ),\n        migrations.AddField(\n            model_name='modelamodelb',\n            name='modela',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'),\n        ),\n        migrations.AddField(\n            model_name='modelamodelb',\n            name='modelb',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'),\n        ),\n    ]\n```\n-----\n\nWe just need to wrap these operations in `migrations.SeparateDatabaseAndState` to get the models in sync without screwing up the existing database setup. All of the changes above represent state changes:\n\n```\nimport datetime\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.Migration):\n\n    dependencies = [\n        ('api_app', '69420_whatever_your_last_migration_was'),\n    ]\n\n    state_operations = [\n        migrations.CreateModel(\n            name='ModelAModelB',\n            fields=[\n                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),\n            ],\n            options={\n                'db_table': 'app_model_a_model_b',\n            },\n        ),\n        migrations.AlterField(\n            model_name='modela',\n            name='b_models',\n            field=models.ManyToManyField(blank=True, related_name='a_models', through='app.ModelAModelB', to='app.ModelB'),\n        ),\n        migrations.AddField(\n            model_name='modelamodelb',\n            name='modela',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelA'),\n        ),\n        migrations.AddField(\n            model_name='modelamodelb',\n            name='modelb',\n            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.ModelB'),\n        ),\n    ]\n\n    operations = [\n        migrations.SeparateDatabaseAndState(\n            state_operations=state_operations,\n        )\n    ]\n```\n-----\n\nAnd that should do it. That migration should successfully tie the existing 'through' table to your new model. Now you can add fields and create additional migrations as normal.\n\n### For my notes - filtering on 'through' fields\n\nI want to make one additional note - since it wasn't immediately obvious to me.  With Django 2.2, you should be able to filter on the 'through' table fields by using the lowercase name of the 'through' model.\n\nYou can add something like `state` to the 'through' model:\n\n```\nclass ModelAModelB(models.Model):\n\n    modela = models.ForeignKey(\"app.ModelA\", on_delete=models.CASCADE)\n    modelb = models.ForeignKey(\"app.ModelB\", on_delete=models.CASCADE)\n    state = models.CharField(\n      max_length=16,\n      null=True,\n      blank=True,\n      choices=[(\"good\", \"good\"), (\"ungood\", \"ungood\")]\n    )\n\n    class Meta:\n        db_table = \"app_model_a_model_b\"\n```\n\nThen filtering on this field looks a lot like this:\n```\n# querysets\nModelA.objects.filter(modelamodelb__state=\"good\")\nModelB.objects.filter(modelamodelb__state=\"ungood\")\nModelAModelB.objects.filter(state=\"good\")\n\n# related manager on instances\na_instance = ModelA.objects.first()\na_instance.b_models.filter(modelamodelb__state=\"ungood\")\n\nb_instance = ModelB.objects.first()\nb_instance.a_models.filter(modelamodelb__state=\"good\")\n```\n-----\n\nNow hopefully I can just come back to this post next time I need to do this.\n\n-----\n*Prefer to catch my posts elsewhere?*\nOriginally posted on my blog: https://typenil.com/hijacking-default-django-through-tables/",
      "json_metadata": "{\"tags\":[\"python\",\"django\",\"migrations\"],\"links\":[\"https://typenil.com/hijacking-default-django-through-tables/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 5.982 SP to @typenil
2019/07/20 21:01:21
delegatorsteem
delegateetypenil
vesting shares9729.930188 VESTS
Transaction InfoBlock #34837645/Trx f85ab027a8604a217c3d9a1b040335bf219638ec
View Raw JSON Data
{
  "trx_id": "f85ab027a8604a217c3d9a1b040335bf219638ec",
  "block": 34837645,
  "trx_in_block": 39,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-07-20T21:01:21",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9729.930188 VESTS"
    }
  ]
}
typenilupdated their account properties
2019/06/21 19:33:36
accounttypenil
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{"profile":{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Developer","website":"https://mdub.dev/","name":"Matt White","location":"United States"}}
Transaction InfoBlock #34001704/Trx ad84b88c9cc1779dd59847a8cf0b9328aebcf654
View Raw JSON Data
{
  "trx_id": "ad84b88c9cc1779dd59847a8cf0b9328aebcf654",
  "block": 34001704,
  "trx_in_block": 9,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-06-21T19:33:36",
  "op": [
    "account_update",
    {
      "account": "typenil",
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Developer\",\"website\":\"https://mdub.dev/\",\"name\":\"Matt White\",\"location\":\"United States\"}}"
    }
  ]
}
steemdelegated 18.295 SP to @typenil
2019/04/20 20:29:36
delegatorsteem
delegateetypenil
vesting shares29757.819257 VESTS
Transaction InfoBlock #32218924/Trx 602ab3cb81a388f1d01ef27ddfa78bcf7c546ccb
View Raw JSON Data
{
  "trx_id": "602ab3cb81a388f1d01ef27ddfa78bcf7c546ccb",
  "block": 32218924,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-20T20:29:36",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "29757.819257 VESTS"
    }
  ]
}
2019/04/20 18:44:57
parent author
parent permlinkpython
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
titleHow to automate Ghost/Medium cross-posting via Zapier
body@@ -5,37 +5,105 @@ l%3E%0A%3C -div class=%22kg-card-markdown%22%3E +p%3EOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%3C/p%3E%0A %3Cp%3EW @@ -1000,17 +1000,16 @@ br%3E%0A%3C/a%3E - + without @@ -1400,16 +1400,17 @@ m-app-3%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -1662,16 +1662,17 @@ apier-0%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -1913,16 +1913,17 @@ apier-1%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2091,16 +2091,17 @@ apier-2%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2306,16 +2306,17 @@ apier-4%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -2524,16 +2524,17 @@ apier-5%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2615,16 +2615,17 @@ apier-6%22 +/ %3E%3C/p%3E%0A%3Cp @@ -3248,38 +3248,17 @@ r-7%22 +/ %3E%3C/p%3E%0A%3C -table%3E%0A%3Cthead%3E%0A%3Ctr%3E%0A%3Cth +p %3EInp @@ -3270,15 +3270,13 @@ me%3C/ -th%3E%0A%3Cth +p%3E%0A%3Cp %3EGho @@ -3288,84 +3288,39 @@ me%3C/ -th%3E%0A%3C/tr%3E%0A%3C/thead%3E%0A%3Ctbody%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Etitle%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3ETitle%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Ecan @@ -3334,39 +3334,24 @@ rl%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3EURL%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Econ @@ -3356,23 +3356,21 @@ ontent%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3EHTML Fo @@ -3390,40 +3390,25 @@ nt%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Etags%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3ETag @@ -3419,34 +3419,9 @@ ug%3C/ -td%3E%0A%3C/tr%3E%0A%3C/tbody%3E%0A%3C/table +p %3E%0A%3Cp @@ -3789,16 +3789,17 @@ pier-10%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -4202,17 +4202,16 @@ a%3E.%3C/p%3E%0A -%0A %3Cp%3E(NOTE @@ -4291,14 +4291,8 @@ %3C/p%3E -%0A%0A%3Chr%3E %0A%3Cp%3E @@ -4495,16 +4495,17 @@ pier-11%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -4736,16 +4736,17 @@ pier-12%22 +/ %3E%3Cbr%3E%0A%3Ci @@ -4826,16 +4826,17 @@ pier-13%22 +/ %3E%3C/p%3E%0A%3Cp @@ -4989,16 +4989,17 @@ pier-14%22 +/ %3E%3C/p%3E%0A%3Cp @@ -5533,13 +5533,8 @@ /p%3E%0A -%3Chr%3E%0A %3Cp%3E%3C @@ -5586,16 +5586,18 @@ p%3E%0A%3Cul%3E%0A + %3Cli%3EType @@ -5672,197 +5672,11 @@ i%3E%0A%3C -li%3EMedium: %3Ca href=%22https://medium.com/@typenil%22%3Ehttps://medium.com/@typenil%3C/a%3E%3C/li%3E%0A%3C/ul%3E%0A%0AOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%0A%3C/div +/ul %3E%0A%3C/
json metadata{"tags":["python","ghost","medium","blogging","zapier"],"app":"steemit/0.1","format":"html","image":["https://typenil.com/content/images/2018/07/medium-app-3.png","https://typenil.com/content/images/2018/07/zapier-0.png","https://typenil.com/content/images/2018/07/zapier-1.png","https://typenil.com/content/images/2018/07/zapier-2.png","https://typenil.com/content/images/2018/07/zapier-4.png","https://typenil.com/content/images/2018/07/zapier-5.png","https://typenil.com/content/images/2018/07/zapier-6.png","https://typenil.com/content/images/2018/07/zapier-7.png","https://typenil.com/content/images/2018/07/zapier-10.png","https://typenil.com/content/images/2018/07/zapier-11.png","https://typenil.com/content/images/2018/07/zapier-12.png","https://typenil.com/content/images/2018/07/zapier-13.png","https://typenil.com/content/images/2018/07/zapier-14.png"],"links":["https://typenil.com/automatic-ghost-medium-cross-posting/","https://cmichel.io/how-to-crosspost-to-medium","http://typenil.com/","https://ghost.org/","https://zapier.com","https://github.com/typenil/ghost-crosspost-medium","https://medium.com/me/settings","https://github.com/Medium/medium-api-docs#creating-a-post","https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py","https://cmichel.io/how-to-crosspost-to-medium/","https://typenil.com/"]}
Transaction InfoBlock #32216833/Trx 09efbcd38af482ad4c4db136ab71cb55c46f4ac7
View Raw JSON Data
{
  "trx_id": "09efbcd38af482ad4c4db136ab71cb55c46f4ac7",
  "block": 32216833,
  "trx_in_block": 23,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-20T18:44:57",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "title": "How to automate Ghost/Medium cross-posting via Zapier",
      "body": "@@ -5,37 +5,105 @@\n l%3E%0A%3C\n-div class=%22kg-card-markdown%22%3E\n+p%3EOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%3C/p%3E%0A\n %3Cp%3EW\n@@ -1000,17 +1000,16 @@\n br%3E%0A%3C/a%3E\n- \n+\n without \n@@ -1400,16 +1400,17 @@\n m-app-3%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -1662,16 +1662,17 @@\n apier-0%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -1913,16 +1913,17 @@\n apier-1%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2091,16 +2091,17 @@\n apier-2%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2306,16 +2306,17 @@\n apier-4%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2524,16 +2524,17 @@\n apier-5%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2615,16 +2615,17 @@\n apier-6%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -3248,38 +3248,17 @@\n r-7%22\n+/\n %3E%3C/p%3E%0A%3C\n-table%3E%0A%3Cthead%3E%0A%3Ctr%3E%0A%3Cth\n+p\n %3EInp\n@@ -3270,15 +3270,13 @@\n me%3C/\n-th%3E%0A%3Cth\n+p%3E%0A%3Cp\n %3EGho\n@@ -3288,84 +3288,39 @@\n me%3C/\n-th%3E%0A%3C/tr%3E%0A%3C/thead%3E%0A%3Ctbody%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Etitle%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3ETitle%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Ecan\n@@ -3334,39 +3334,24 @@\n rl%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3EURL%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Econ\n@@ -3356,23 +3356,21 @@\n ontent%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3EHTML Fo\n@@ -3390,40 +3390,25 @@\n nt%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Etags%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3ETag\n@@ -3419,34 +3419,9 @@\n ug%3C/\n-td%3E%0A%3C/tr%3E%0A%3C/tbody%3E%0A%3C/table\n+p\n %3E%0A%3Cp\n@@ -3789,16 +3789,17 @@\n pier-10%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4202,17 +4202,16 @@\n a%3E.%3C/p%3E%0A\n-%0A\n %3Cp%3E(NOTE\n@@ -4291,14 +4291,8 @@\n %3C/p%3E\n-%0A%0A%3Chr%3E\n %0A%3Cp%3E\n@@ -4495,16 +4495,17 @@\n pier-11%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4736,16 +4736,17 @@\n pier-12%22\n+/\n %3E%3Cbr%3E%0A%3Ci\n@@ -4826,16 +4826,17 @@\n pier-13%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4989,16 +4989,17 @@\n pier-14%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -5533,13 +5533,8 @@\n /p%3E%0A\n-%3Chr%3E%0A\n %3Cp%3E%3C\n@@ -5586,16 +5586,18 @@\n p%3E%0A%3Cul%3E%0A\n+  \n %3Cli%3EType\n@@ -5672,197 +5672,11 @@\n i%3E%0A%3C\n-li%3EMedium: %3Ca href=%22https://medium.com/@typenil%22%3Ehttps://medium.com/@typenil%3C/a%3E%3C/li%3E%0A%3C/ul%3E%0A%0AOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%0A%3C/div\n+/ul\n %3E%0A%3C/\n",
      "json_metadata": "{\"tags\":[\"python\",\"ghost\",\"medium\",\"blogging\",\"zapier\"],\"app\":\"steemit/0.1\",\"format\":\"html\",\"image\":[\"https://typenil.com/content/images/2018/07/medium-app-3.png\",\"https://typenil.com/content/images/2018/07/zapier-0.png\",\"https://typenil.com/content/images/2018/07/zapier-1.png\",\"https://typenil.com/content/images/2018/07/zapier-2.png\",\"https://typenil.com/content/images/2018/07/zapier-4.png\",\"https://typenil.com/content/images/2018/07/zapier-5.png\",\"https://typenil.com/content/images/2018/07/zapier-6.png\",\"https://typenil.com/content/images/2018/07/zapier-7.png\",\"https://typenil.com/content/images/2018/07/zapier-10.png\",\"https://typenil.com/content/images/2018/07/zapier-11.png\",\"https://typenil.com/content/images/2018/07/zapier-12.png\",\"https://typenil.com/content/images/2018/07/zapier-13.png\",\"https://typenil.com/content/images/2018/07/zapier-14.png\"],\"links\":[\"https://typenil.com/automatic-ghost-medium-cross-posting/\",\"https://cmichel.io/how-to-crosspost-to-medium\",\"http://typenil.com/\",\"https://ghost.org/\",\"https://zapier.com\",\"https://github.com/typenil/ghost-crosspost-medium\",\"https://medium.com/me/settings\",\"https://github.com/Medium/medium-api-docs#creating-a-post\",\"https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py\",\"https://cmichel.io/how-to-crosspost-to-medium/\",\"https://typenil.com/\"]}"
    }
  ]
}
2019/04/20 18:44:18
parent author
parent permlinkpython
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
titleHow to automate Ghost/Medium cross-posting via Zapier
body@@ -5,37 +5,105 @@ l%3E%0A%3C -div class=%22kg-card-markdown%22%3E +p%3EOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%3C/p%3E%0A %3Cp%3EW @@ -1000,17 +1000,16 @@ br%3E%0A%3C/a%3E - + without @@ -1400,16 +1400,17 @@ m-app-3%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -1662,16 +1662,17 @@ apier-0%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -1913,16 +1913,17 @@ apier-1%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2091,16 +2091,17 @@ apier-2%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2306,16 +2306,17 @@ apier-4%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -2524,16 +2524,17 @@ apier-5%22 +/ %3E%3C/p%3E%0A%3Cp @@ -2615,16 +2615,17 @@ apier-6%22 +/ %3E%3C/p%3E%0A%3Cp @@ -3248,38 +3248,17 @@ r-7%22 +/ %3E%3C/p%3E%0A%3C -table%3E%0A%3Cthead%3E%0A%3Ctr%3E%0A%3Cth +p %3EInp @@ -3270,15 +3270,13 @@ me%3C/ -th%3E%0A%3Cth +p%3E%0A%3Cp %3EGho @@ -3288,84 +3288,39 @@ me%3C/ -th%3E%0A%3C/tr%3E%0A%3C/thead%3E%0A%3Ctbody%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Etitle%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3ETitle%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Ecan @@ -3334,39 +3334,24 @@ rl%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3EURL%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Econ @@ -3356,23 +3356,21 @@ ontent%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3EHTML Fo @@ -3390,40 +3390,25 @@ nt%3C/ -td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd +p%3E%0A%3Cp %3Etags%3C/ -td%3E%0A%3Ctd +p%3E%0A%3Cp %3ETag @@ -3419,34 +3419,9 @@ ug%3C/ -td%3E%0A%3C/tr%3E%0A%3C/tbody%3E%0A%3C/table +p %3E%0A%3Cp @@ -3789,16 +3789,17 @@ pier-10%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -4202,17 +4202,16 @@ a%3E.%3C/p%3E%0A -%0A %3Cp%3E(NOTE @@ -4291,14 +4291,8 @@ %3C/p%3E -%0A%0A%3Chr%3E %0A%3Cp%3E @@ -4495,16 +4495,17 @@ pier-11%22 - +/ %3E%3C/p%3E%0A%3Cp @@ -4736,16 +4736,17 @@ pier-12%22 +/ %3E%3Cbr%3E%0A%3Ci @@ -4826,16 +4826,17 @@ pier-13%22 +/ %3E%3C/p%3E%0A%3Cp @@ -4989,16 +4989,17 @@ pier-14%22 +/ %3E%3C/p%3E%0A%3Cp @@ -5533,13 +5533,8 @@ /p%3E%0A -%3Chr%3E%0A %3Cp%3E%3C @@ -5586,16 +5586,18 @@ p%3E%0A%3Cul%3E%0A + %3Cli%3EType @@ -5672,197 +5672,11 @@ i%3E%0A%3C -li%3EMedium: %3Ca href=%22https://medium.com/@typenil%22%3Ehttps://medium.com/@typenil%3C/a%3E%3C/li%3E%0A%3C/ul%3E%0A%0AOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%0A%3C/div +/ul %3E%0A%3C/
json metadata{"tags":["python","ghost","medium","blogging","zapier"],"app":"steemit/0.1","format":"html","image":["https://typenil.com/content/images/2018/07/medium-app-3.png","https://typenil.com/content/images/2018/07/zapier-0.png","https://typenil.com/content/images/2018/07/zapier-1.png","https://typenil.com/content/images/2018/07/zapier-2.png","https://typenil.com/content/images/2018/07/zapier-4.png","https://typenil.com/content/images/2018/07/zapier-5.png","https://typenil.com/content/images/2018/07/zapier-6.png","https://typenil.com/content/images/2018/07/zapier-7.png","https://typenil.com/content/images/2018/07/zapier-10.png","https://typenil.com/content/images/2018/07/zapier-11.png","https://typenil.com/content/images/2018/07/zapier-12.png","https://typenil.com/content/images/2018/07/zapier-13.png","https://typenil.com/content/images/2018/07/zapier-14.png"],"links":["https://typenil.com/automatic-ghost-medium-cross-posting/","https://cmichel.io/how-to-crosspost-to-medium","http://typenil.com/","https://ghost.org/","https://zapier.com","https://github.com/typenil/ghost-crosspost-medium","https://medium.com/me/settings","https://github.com/Medium/medium-api-docs#creating-a-post","https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py","https://cmichel.io/how-to-crosspost-to-medium/","https://typenil.com/"]}
Transaction InfoBlock #32216820/Trx c7421111315a24af2df8d6fdaa9f47217d38205d
View Raw JSON Data
{
  "trx_id": "c7421111315a24af2df8d6fdaa9f47217d38205d",
  "block": 32216820,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-20T18:44:18",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "title": "How to automate Ghost/Medium cross-posting via Zapier",
      "body": "@@ -5,37 +5,105 @@\n l%3E%0A%3C\n-div class=%22kg-card-markdown%22%3E\n+p%3EOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%3C/p%3E%0A\n %3Cp%3EW\n@@ -1000,17 +1000,16 @@\n br%3E%0A%3C/a%3E\n- \n+\n without \n@@ -1400,16 +1400,17 @@\n m-app-3%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -1662,16 +1662,17 @@\n apier-0%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -1913,16 +1913,17 @@\n apier-1%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2091,16 +2091,17 @@\n apier-2%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2306,16 +2306,17 @@\n apier-4%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2524,16 +2524,17 @@\n apier-5%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -2615,16 +2615,17 @@\n apier-6%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -3248,38 +3248,17 @@\n r-7%22\n+/\n %3E%3C/p%3E%0A%3C\n-table%3E%0A%3Cthead%3E%0A%3Ctr%3E%0A%3Cth\n+p\n %3EInp\n@@ -3270,15 +3270,13 @@\n me%3C/\n-th%3E%0A%3Cth\n+p%3E%0A%3Cp\n %3EGho\n@@ -3288,84 +3288,39 @@\n me%3C/\n-th%3E%0A%3C/tr%3E%0A%3C/thead%3E%0A%3Ctbody%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Etitle%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3ETitle%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Ecan\n@@ -3334,39 +3334,24 @@\n rl%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3EURL%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Econ\n@@ -3356,23 +3356,21 @@\n ontent%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3EHTML Fo\n@@ -3390,40 +3390,25 @@\n nt%3C/\n-td%3E%0A%3C/tr%3E%0A%3Ctr%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3Etags%3C/\n-td%3E%0A%3Ctd\n+p%3E%0A%3Cp\n %3ETag\n@@ -3419,34 +3419,9 @@\n ug%3C/\n-td%3E%0A%3C/tr%3E%0A%3C/tbody%3E%0A%3C/table\n+p\n %3E%0A%3Cp\n@@ -3789,16 +3789,17 @@\n pier-10%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4202,17 +4202,16 @@\n a%3E.%3C/p%3E%0A\n-%0A\n %3Cp%3E(NOTE\n@@ -4291,14 +4291,8 @@\n %3C/p%3E\n-%0A%0A%3Chr%3E\n %0A%3Cp%3E\n@@ -4495,16 +4495,17 @@\n pier-11%22\n-\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4736,16 +4736,17 @@\n pier-12%22\n+/\n %3E%3Cbr%3E%0A%3Ci\n@@ -4826,16 +4826,17 @@\n pier-13%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -4989,16 +4989,17 @@\n pier-14%22\n+/\n %3E%3C/p%3E%0A%3Cp\n@@ -5533,13 +5533,8 @@\n /p%3E%0A\n-%3Chr%3E%0A\n %3Cp%3E%3C\n@@ -5586,16 +5586,18 @@\n p%3E%0A%3Cul%3E%0A\n+  \n %3Cli%3EType\n@@ -5672,197 +5672,11 @@\n i%3E%0A%3C\n-li%3EMedium: %3Ca href=%22https://medium.com/@typenil%22%3Ehttps://medium.com/@typenil%3C/a%3E%3C/li%3E%0A%3C/ul%3E%0A%0AOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/%0A%3C/div\n+/ul\n %3E%0A%3C/\n",
      "json_metadata": "{\"tags\":[\"python\",\"ghost\",\"medium\",\"blogging\",\"zapier\"],\"app\":\"steemit/0.1\",\"format\":\"html\",\"image\":[\"https://typenil.com/content/images/2018/07/medium-app-3.png\",\"https://typenil.com/content/images/2018/07/zapier-0.png\",\"https://typenil.com/content/images/2018/07/zapier-1.png\",\"https://typenil.com/content/images/2018/07/zapier-2.png\",\"https://typenil.com/content/images/2018/07/zapier-4.png\",\"https://typenil.com/content/images/2018/07/zapier-5.png\",\"https://typenil.com/content/images/2018/07/zapier-6.png\",\"https://typenil.com/content/images/2018/07/zapier-7.png\",\"https://typenil.com/content/images/2018/07/zapier-10.png\",\"https://typenil.com/content/images/2018/07/zapier-11.png\",\"https://typenil.com/content/images/2018/07/zapier-12.png\",\"https://typenil.com/content/images/2018/07/zapier-13.png\",\"https://typenil.com/content/images/2018/07/zapier-14.png\"],\"links\":[\"https://typenil.com/automatic-ghost-medium-cross-posting/\",\"https://cmichel.io/how-to-crosspost-to-medium\",\"http://typenil.com/\",\"https://ghost.org/\",\"https://zapier.com\",\"https://github.com/typenil/ghost-crosspost-medium\",\"https://medium.com/me/settings\",\"https://github.com/Medium/medium-api-docs#creating-a-post\",\"https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py\",\"https://cmichel.io/how-to-crosspost-to-medium/\",\"https://typenil.com/\"]}"
    }
  ]
}
typenilpublished a new post: github-oauth2-in-go
2019/04/20 18:40:51
parent author
parent permlinkgolang
authortypenil
permlinkgithub-oauth2-in-go
titleGitHub OAuth2 in Go
bodyI've been playing around with Git repository analysis tools recently (my favorite of which is the now-defunct [gitinspector](https://github.com/ejwa/gitinspector)), tracking my contributions on a regular basis. These tools can only tell you so much, so I've been building out a project to replace my weekly spreadsheet entries. Integrating with GitHub - a treasure trove of contribution data - seemed an inevitability. Signing up for the [GitHub Developer Program](https://developer.github.com/program/) is pretty straightforward, so I won't cover it here. You provide a URL and a support email and poof - you're a developer. If you prefer to jump to the end, you can probably get what you need from this post's [accompanying repo](https://github.com/typenil/golang-github-oauth). ![Key - Matt Artz](https://cdn.steemitimages.com/DQmPrgWX5uyGwWWcND9aMCjbJAHyVDgRr2r8bKnqztjJCHV/matt-artz-353284-unsplash.jpg) We'll be looking at how to set up a basic OAuth2 flow in Golang (at which I am a beginner). GitHub covers the same ground as this post quite well [here](https://developer.github.com/v3/guides/basics-of-authentication/), albeit in Ruby. Luckily, Go already has the [oauth2 package](https://godoc.org/golang.org/x/oauth2) at the ready, which you can install with `go get golang.org/x/oauth2`. The oauth2 package uses a `Config` to represent the standard OAuth flow. ``` conf := &oauth2.Config{ ClientID: "YOUR_CLIENT_ID", ClientSecret: "YOUR_CLIENT_SECRET", Scopes: []string{"SCOPE1", "SCOPE2"}, Endpoint: oauth2.Endpoint{ AuthURL: "https://provider.com/o/oauth2/auth", TokenURL: "https://provider.com/o/oauth2/token", }, } ``` This makes it pretty clear what we need in order to authenticate. In our case, both the `ClientID` and the `ClientSecret` will come from GitHub, so we'll need to setup a [new OAuth application](https://github.com/settings/applications/new). ![New OAuth Application](https://cdn.steemitimages.com/DQmY7cXi4nWzSrnvj65icKQ48fg8qKDzgi15hdx4XFeidsV/github-0.png) You can use any values for the application name and homepage url. For our purposes, use http://localhost:4567/callback for the callback. Once you've created your application, you should be redirected to a page with your own `ClientID` and `ClientSecret`. To keep them out of the codebase, let's store the GitHub credentials in environment variables by exporting them: ``` export GITHUB_CLIENT_ID=<your GitHub client id> export GITHUB_CLIENT_SECRET=<your GitHub client secret> ``` With those in place, we can create our `Config`: ``` package main import ( "golang.org/x/oauth2" "golang.org/x/oauth2/github" "os" ) func main() { conf := &oauth2.Config{ ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), Scopes: []string{"public_repo"}, Endpoint: github.Endpoint, } } ``` Here we're using the included `github.Endpoint` constant and limiting ourselves to a `public_repo` scope. GitHub provides a list of [available scopes](https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes) from which you can pick. From here, it's surprisingly easy to get integrated. We need to call `AuthCodeURL` to get the OAuth redirect and we need a basic server to handle the callback and token exchange with GitHub. ``` url := conf.AuthCodeURL("", oauth2.AccessTypeOffline) fmt.Printf("Login with GitHub: %v", url) http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request){ err := r.ParseForm() if err != nil { log.Fatal(err) } token, err := conf.Exchange(ctx, r.Form["code"][0]) if err != nil { log.Fatal(err) } fmt.Fprintf(w, "Token: %s", token) }) log.Fatal(http.ListenAndServe(":4567", nil)) ``` At this point, you can run `go run main.go`, click on the link, authenticate with GitHub, and GitHub will callback to your simple server that retrieves your authentication token. But let's actually do something with the GitHub API. For my selfish purposes, I want to be interacting with the repository API endpoints. Let's make a call to the repo list endpoint and display it on the callback page. Altogether: ``` package main import ( "context" "fmt" "golang.org/x/oauth2" "golang.org/x/oauth2/github" "io/ioutil" "log" "net/http" "os" ) func main() { ctx := context.Background() conf := &oauth2.Config{ ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), Scopes: []string{"public_repo"}, Endpoint: github.Endpoint, } url := conf.AuthCodeURL("", oauth2.AccessTypeOffline) fmt.Printf("Login with GitHub: %v", url) http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request){ err := r.ParseForm() if err != nil { log.Fatal(err) } token, err := conf.Exchange(ctx, r.Form["code"][0]) if err != nil { log.Fatal(err) } client := conf.Client(ctx, token) response, err := client.Get("https://api.github.com/user/repos?page=0&per_page=100") if err != nil { log.Fatal(err) } defer response.Body.Close() repos, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } fmt.Fprintf(w, "Repos: %s", repos) }) log.Fatal(http.ListenAndServe(":4567", nil)) } ``` The response from GitHub is a mess of JSON - which will be far more useful if you [deserialize it](https://gobyexample.com/json) first. This is obviously a very bare bones authentication flow. For fleshing out the implementation a bit more, I recommend Gergely Brautigam's [How to do Google sign-in with Go](https://skarlso.github.io/2016/06/12/google-signin-with-go/). ----------- *Prefer to catch my posts elsewhere?* Originally published on my blog: [https://typenil.com/posts/golang-github-oauth/](https://typenil.com/posts/golang-github-oauth/)
json metadata{"tags":["golang","github","oauth","go"],"image":["https://cdn.steemitimages.com/DQmPrgWX5uyGwWWcND9aMCjbJAHyVDgRr2r8bKnqztjJCHV/matt-artz-353284-unsplash.jpg","https://cdn.steemitimages.com/DQmY7cXi4nWzSrnvj65icKQ48fg8qKDzgi15hdx4XFeidsV/github-0.png"],"links":["https://github.com/ejwa/gitinspector","https://developer.github.com/program/","https://github.com/typenil/golang-github-oauth","https://developer.github.com/v3/guides/basics-of-authentication/","https://godoc.org/golang.org/x/oauth2","https://github.com/settings/applications/new","http://localhost:4567/callback","https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes","https://gobyexample.com/json","https://skarlso.github.io/2016/06/12/google-signin-with-go/","https://typenil.com/posts/golang-github-oauth/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #32216751/Trx 9900b710181503b6c90d846462f5f5099b983e6a
View Raw JSON Data
{
  "trx_id": "9900b710181503b6c90d846462f5f5099b983e6a",
  "block": 32216751,
  "trx_in_block": 57,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-20T18:40:51",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "golang",
      "author": "typenil",
      "permlink": "github-oauth2-in-go",
      "title": "GitHub OAuth2 in Go",
      "body": "I've been playing around with Git repository analysis tools recently (my favorite of which is the now-defunct [gitinspector](https://github.com/ejwa/gitinspector)), tracking my contributions on a regular basis. These tools can only tell you so much, so I've been building out a project to replace my weekly spreadsheet entries. Integrating with GitHub - a treasure trove of contribution data - seemed an inevitability.\n\nSigning up for the [GitHub Developer Program](https://developer.github.com/program/) is pretty straightforward, so I won't cover it here. You provide a URL and a support email and poof - you're a developer.\n\nIf you prefer to jump to the end, you can probably get what you need from this post's [accompanying repo](https://github.com/typenil/golang-github-oauth).\n\n![Key - Matt Artz](https://cdn.steemitimages.com/DQmPrgWX5uyGwWWcND9aMCjbJAHyVDgRr2r8bKnqztjJCHV/matt-artz-353284-unsplash.jpg)\n\nWe'll be looking at how to set up a basic OAuth2 flow in Golang (at which I am a beginner). GitHub covers the same ground as this post quite well [here](https://developer.github.com/v3/guides/basics-of-authentication/), albeit in Ruby.\n\nLuckily, Go already has the [oauth2 package](https://godoc.org/golang.org/x/oauth2) at the ready, which you can install with `go get golang.org/x/oauth2`. The oauth2 package uses a `Config` to represent the standard OAuth flow.\n```\nconf := &oauth2.Config{\n    ClientID:     \"YOUR_CLIENT_ID\",\n    ClientSecret: \"YOUR_CLIENT_SECRET\",\n    Scopes:       []string{\"SCOPE1\", \"SCOPE2\"},\n    Endpoint: oauth2.Endpoint{\n        AuthURL:  \"https://provider.com/o/oauth2/auth\",\n        TokenURL: \"https://provider.com/o/oauth2/token\",\n    },\n}\n```\nThis makes it pretty clear what we need in order to authenticate. In our case, both the `ClientID` and the `ClientSecret` will come from GitHub, so we'll need to setup a [new OAuth application](https://github.com/settings/applications/new).\n\n![New OAuth Application](https://cdn.steemitimages.com/DQmY7cXi4nWzSrnvj65icKQ48fg8qKDzgi15hdx4XFeidsV/github-0.png)\n\nYou can use any values for the application name and homepage url. For our purposes, use http://localhost:4567/callback for the callback.\n\nOnce you've created your application, you should be redirected to a page with your own `ClientID` and `ClientSecret`. To keep them out of the codebase, let's store the GitHub credentials in environment variables by exporting them:\n```\nexport GITHUB_CLIENT_ID=<your GitHub client id>\nexport GITHUB_CLIENT_SECRET=<your GitHub client secret>\n```\n\nWith those in place, we can create our `Config`:\n\n```\npackage main\n\nimport (\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/github\"\n\t\"os\"\n)\n\nfunc main() {\n\tconf := &oauth2.Config{\n\t\tClientID:     os.Getenv(\"GITHUB_CLIENT_ID\"),\n\t\tClientSecret: os.Getenv(\"GITHUB_CLIENT_SECRET\"),\n\t\tScopes:       []string{\"public_repo\"},\n\t\tEndpoint: github.Endpoint,\n\t}\n}\n\n```\n\nHere we're using the included `github.Endpoint` constant and limiting ourselves to a `public_repo` scope. GitHub provides a list of [available scopes](https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes) from which you can pick.\n\nFrom here, it's surprisingly easy to get integrated. We need to call `AuthCodeURL` to get the OAuth redirect and we need a basic server to handle the callback and token exchange with GitHub.\n\n```\n\turl := conf.AuthCodeURL(\"\", oauth2.AccessTypeOffline)\n\tfmt.Printf(\"Login with GitHub: %v\", url)\n\n\n\thttp.HandleFunc(\"/callback\", func(w http.ResponseWriter, r *http.Request){\n\t\terr := r.ParseForm()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\ttoken, err := conf.Exchange(ctx, r.Form[\"code\"][0])\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tfmt.Fprintf(w, \"Token: %s\", token)\n\t})\n\n\tlog.Fatal(http.ListenAndServe(\":4567\", nil))\n```\nAt this point, you can run `go run main.go`, click on the link, authenticate with GitHub, and GitHub will callback to your simple server that retrieves your authentication token. But let's actually do something with the GitHub API.\n\nFor my selfish purposes, I want to be interacting with the repository API endpoints. Let's make a call to the repo list endpoint and display it on the callback page. Altogether:\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/github\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tconf := &oauth2.Config{\n\t\tClientID:     os.Getenv(\"GITHUB_CLIENT_ID\"),\n\t\tClientSecret: os.Getenv(\"GITHUB_CLIENT_SECRET\"),\n\t\tScopes:       []string{\"public_repo\"},\n\t\tEndpoint: github.Endpoint,\n\t}\n\turl := conf.AuthCodeURL(\"\", oauth2.AccessTypeOffline)\n\tfmt.Printf(\"Login with GitHub: %v\", url)\n\n\n\thttp.HandleFunc(\"/callback\", func(w http.ResponseWriter, r *http.Request){\n\t\terr := r.ParseForm()\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\ttoken, err := conf.Exchange(ctx, r.Form[\"code\"][0])\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tclient := conf.Client(ctx, token)\n\t\tresponse, err := client.Get(\"https://api.github.com/user/repos?page=0&per_page=100\")\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tdefer response.Body.Close()\n\t\trepos, err := ioutil.ReadAll(response.Body)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tfmt.Fprintf(w, \"Repos: %s\", repos)\n\t})\n\n\tlog.Fatal(http.ListenAndServe(\":4567\", nil))\n}\n```\n\nThe response from GitHub is a mess of JSON - which will be far more useful if you [deserialize it](https://gobyexample.com/json) first.\n\nThis is obviously a very bare bones authentication flow. For fleshing out the implementation a bit more, I recommend Gergely Brautigam's [How to do Google sign-in with Go](https://skarlso.github.io/2016/06/12/google-signin-with-go/).\n\n-----------\n*Prefer to catch my posts elsewhere?*\n\nOriginally published on my blog: [https://typenil.com/posts/golang-github-oauth/](https://typenil.com/posts/golang-github-oauth/)",
      "json_metadata": "{\"tags\":[\"golang\",\"github\",\"oauth\",\"go\"],\"image\":[\"https://cdn.steemitimages.com/DQmPrgWX5uyGwWWcND9aMCjbJAHyVDgRr2r8bKnqztjJCHV/matt-artz-353284-unsplash.jpg\",\"https://cdn.steemitimages.com/DQmY7cXi4nWzSrnvj65icKQ48fg8qKDzgi15hdx4XFeidsV/github-0.png\"],\"links\":[\"https://github.com/ejwa/gitinspector\",\"https://developer.github.com/program/\",\"https://github.com/typenil/golang-github-oauth\",\"https://developer.github.com/v3/guides/basics-of-authentication/\",\"https://godoc.org/golang.org/x/oauth2\",\"https://github.com/settings/applications/new\",\"http://localhost:4567/callback\",\"https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes\",\"https://gobyexample.com/json\",\"https://skarlso.github.io/2016/06/12/google-signin-with-go/\",\"https://typenil.com/posts/golang-github-oauth/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 6.019 SP to @typenil
2019/04/07 04:28:27
delegatorsteem
delegateetypenil
vesting shares9790.913413 VESTS
Transaction InfoBlock #31827111/Trx bdd910195a2a9a7ba86025328c87f516bd64110e
View Raw JSON Data
{
  "trx_id": "bdd910195a2a9a7ba86025328c87f516bd64110e",
  "block": 31827111,
  "trx_in_block": 18,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-04-07T04:28:27",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9790.913413 VESTS"
    }
  ]
}
2019/03/02 22:13:18
parent authortypenil
parent permlinkgit-hooks-autoformat-before-commit
authorsteemitboard
permlinksteemitboard-notify-typenil-20190302t221317000z
title
bodyCongratulations @typenil! You received a personal award! <table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@typenil/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table> <sub>_[Click here to view your Board](https://steemitboard.com/@typenil)_</sub> **Do not miss the last post from @steemitboard:** <table><tr><td><a href="https://steemit.com/carnival/@steemitboard/carnival-2019"><img src="https://steemitimages.com/64x128/http://i.cubeupload.com/rltzHT.png"></a></td><td><a href="https://steemit.com/carnival/@steemitboard/carnival-2019">Carnival Challenge - Collect badge and win 5 STEEM</a></td></tr></table> ###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) and get one more award and increased upvotes!
json metadata{"image":["https://steemitboard.com/img/notify.png"]}
Transaction InfoBlock #30812292/Trx 301ac16e1bef402c1b5affd6080206fbc9080d5e
View Raw JSON Data
{
  "trx_id": "301ac16e1bef402c1b5affd6080206fbc9080d5e",
  "block": 30812292,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-03-02T22:13:18",
  "op": [
    "comment",
    {
      "parent_author": "typenil",
      "parent_permlink": "git-hooks-autoformat-before-commit",
      "author": "steemitboard",
      "permlink": "steemitboard-notify-typenil-20190302t221317000z",
      "title": "",
      "body": "Congratulations @typenil! You received a personal award!\n\n<table><tr><td>https://steemitimages.com/70x70/http://steemitboard.com/@typenil/birthday1.png</td><td>Happy Birthday! - You are on the Steem blockchain for 1 year!</td></tr></table>\n\n<sub>_[Click here to view your Board](https://steemitboard.com/@typenil)_</sub>\n\n\n**Do not miss the last post from @steemitboard:**\n<table><tr><td><a href=\"https://steemit.com/carnival/@steemitboard/carnival-2019\"><img src=\"https://steemitimages.com/64x128/http://i.cubeupload.com/rltzHT.png\"></a></td><td><a href=\"https://steemit.com/carnival/@steemitboard/carnival-2019\">Carnival Challenge - Collect badge and win 5 STEEM</a></td></tr></table>\n\n###### [Vote for @Steemitboard as a witness](https://v2.steemconnect.com/sign/account-witness-vote?witness=steemitboard&approve=1) and get one more award and increased upvotes!",
      "json_metadata": "{\"image\":[\"https://steemitboard.com/img/notify.png\"]}"
    }
  ]
}
2019/02/26 01:45:54
parent authortypenil
parent permlinkgit-hooks-autoformat-before-commit
authorpartiko
permlinkpartiko-re-typenil-git-hooks-autoformat-before-commit-20190226t014554028z
title
bodyHello @typenil! This is a friendly reminder that you have 3000 Partiko Points unclaimed in your Partiko account! Partiko is a fast and beautiful mobile app for Steem, and it’s the most popular Steem mobile app out there! Download Partiko using the link below and login using SteemConnect to claim your 3000 Partiko points! You can easily convert them into Steem token! https://partiko.app/referral/partiko ![](https://d1vof77qrk4l5q.cloudfront.net/statics/partiko-poster-best-steem-app-for-your-phone.jpg)
json metadata{"app":"partiko"}
Transaction InfoBlock #30672640/Trx d9c5026f1a0314be94fd4a51e3645a17c3195f71
View Raw JSON Data
{
  "trx_id": "d9c5026f1a0314be94fd4a51e3645a17c3195f71",
  "block": 30672640,
  "trx_in_block": 6,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-02-26T01:45:54",
  "op": [
    "comment",
    {
      "parent_author": "typenil",
      "parent_permlink": "git-hooks-autoformat-before-commit",
      "author": "partiko",
      "permlink": "partiko-re-typenil-git-hooks-autoformat-before-commit-20190226t014554028z",
      "title": "",
      "body": "Hello @typenil! This is a friendly reminder that you have 3000 Partiko Points unclaimed in your Partiko account!\n\nPartiko is a fast and beautiful mobile app for Steem, and it’s the most popular Steem mobile app out there! Download Partiko using the link below and login using SteemConnect to claim your 3000 Partiko points! You can easily convert them into Steem token!\n\nhttps://partiko.app/referral/partiko\n\n![](https://d1vof77qrk4l5q.cloudfront.net/statics/partiko-poster-best-steem-app-for-your-phone.jpg)",
      "json_metadata": "{\"app\":\"partiko\"}"
    }
  ]
}
steemdelegated 18.405 SP to @typenil
2019/01/06 00:10:24
delegatorsteem
delegateetypenil
vesting shares29937.776900 VESTS
Transaction InfoBlock #29203202/Trx 5b895ef174602e9af66676d6bb185dff01728202
View Raw JSON Data
{
  "trx_id": "5b895ef174602e9af66676d6bb185dff01728202",
  "block": 29203202,
  "trx_in_block": 30,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-01-06T00:10:24",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "29937.776900 VESTS"
    }
  ]
}
2019/01/05 22:57:33
voterfyrstikken
authortypenil
permlinkgit-hooks-autoformat-before-commit
weight100 (1.00%)
Transaction InfoBlock #29201745/Trx a1f985567bd18eb368f4e1b4d054a48f22674ad5
View Raw JSON Data
{
  "trx_id": "a1f985567bd18eb368f4e1b4d054a48f22674ad5",
  "block": 29201745,
  "trx_in_block": 34,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-01-05T22:57:33",
  "op": [
    "vote",
    {
      "voter": "fyrstikken",
      "author": "typenil",
      "permlink": "git-hooks-autoformat-before-commit",
      "weight": 100
    }
  ]
}
allazsent 0.001 STEEM to @typenil- "Promote your post. Your post will be min. 10 resteemed with over 13000 followers and min. 25 Upvote Different account (5000 STEEM POWER). Your post will be more popular and you will find new frien..."
2019/01/05 22:43:27
fromallaz
totypenil
amount0.001 STEEM
memoPromote your post. Your post will be min. 10 resteemed with over 13000 followers and min. 25 Upvote Different account (5000 STEEM POWER). Your post will be more popular and you will find new friends. Send 0.5 SBD or STEEM to @allaz (post URL as memo ) Service Active.
Transaction InfoBlock #29201463/Trx b6261850a6e27ec46ae7bc0758dc821ad73eeed5
View Raw JSON Data
{
  "trx_id": "b6261850a6e27ec46ae7bc0758dc821ad73eeed5",
  "block": 29201463,
  "trx_in_block": 14,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-01-05T22:43:27",
  "op": [
    "transfer",
    {
      "from": "allaz",
      "to": "typenil",
      "amount": "0.001 STEEM",
      "memo": "Promote your post. Your post will be min. 10  resteemed with over 13000  followers and min. 25  Upvote Different account (5000  STEEM POWER). Your post will be more popular and you will find new friends. Send 0.5 SBD or STEEM to @allaz (post URL as memo ) Service Active."
    }
  ]
}
2019/01/05 22:40:15
parent author
parent permlinkformatting
authortypenil
permlinkgit-hooks-autoformat-before-commit
titleGit Hooks: Autoformat before commit
bodyAutoformatters can be great, keeping diffs small and a code base readable across teams and engineers. [Black](https://github.com/ambv/black) formatting has been enforced in [Lobit](https://www.lobit.io/) builds since day one and I recently added it to the repos at [Downstream](https://www.downstreamimpact.com/) as well. But what isn't great is forgetting to run the formatter, having the CI build fail, and having to add a "Formatting" commit. That's where [Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) come in. Git hooks are stored in `.git/hooks` folder of any given Git project. There should be some samples pre-populated. For our purposes, `pre-commit` is the hook of interest. Lets focus on the Black formatter for Python as an example. For the example, it needs to be installed locally (`pip install black`). Naively, we can add formatting on pre-commit by adding a file called `pre-commit` to `.git/hooks` with these contents: ``` black . ``` (For Javascript, you might have something like `prettier --write **/*.js`) Don't forget to make the file executable (use `chmod +x .git/hooks/pre-commit`), or the hook may not fire. It's nice and simple, but there are a couple issues with this approach. First off, Black will be running over all the source files rather than just the ones that changed (which can be quite slow, depending on the size of your project). Secondly, the modified source files won't actually be added to the commit, which means you need to re-commit the reformatted files. Here comes the convoluted piping. To get the names and statuses of files that are staged for commit, we can use `git diff --cached --name-status` to yield output like this: ``` > $ git diff --cached --name-status M someproject/src/whatevs.py A someproject/tests/whatevs_test.py D someproject/tests/lasers.py M someproject/README.md ``` -------------- We want to ignore deleted files, since we can't format them, so let's filter them out: ``` > $ git diff --cached --name-status | grep -v '^D' M someproject/src/whatevs.py A someproject/tests/whatevs_test.py M someproject/README.md ``` -------------- We want to ignore non-Python files (or Javascript or Golang or whatever you're using), so let's filter on file extension: ``` > $ git diff --cached --name-status | grep -v '^D' | grep '.py' M someproject/src/whatevs.py A someproject/tests/whatevs_test.py ``` -------------- Now that we have the files we want, we just want to focus on the names: ``` > $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \t]*//' someproject/src/whatevs.py someproject/tests/whatevs_test.py ``` -------------- And we can pass that right into Black (or whatever formatter you're using): ``` > $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \t]*//' | xargs black reformatted someproject/src/whatevs.py reformatted someproject/tests/whatevs_test.py All done! ✨ 🍰 ✨ 2 files reformatted. ``` -------------- So that's all great, but the changes still aren't staged. Let's fix that by processing the output from Black into just the files that were reformatted: ``` > $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \t]//' someproject/src/whatevs.py someproject/tests/whatevs_test.py ``` -------------- And now we can finally stage the formatted files: ``` > $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \t]//' | xargs git add ``` -------------- So the final `.git/hooks/pre-commit` file should look something like [this](https://gist.github.com/typenil/14eb5fbd68798bc1aa8179d96d1cc378): ``` git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \t]//' | xargs git add ``` -------------- Set it and forget it. Happy coding. -------------- *Prefer to catch my posts elsewhere?* Orginally published on my blog: [https://typenil.com/git-hooks-autoformat-before-commit/](https://typenil.com/git-hooks-autoformat-before-commit/)
json metadata{"tags":["formatting","code-quality","git","python","hooks"],"links":["https://github.com/ambv/black","https://www.lobit.io/","https://www.downstreamimpact.com/","https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks","https://gist.github.com/typenil/14eb5fbd68798bc1aa8179d96d1cc378","https://typenil.com/git-hooks-autoformat-before-commit/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #29201399/Trx 7524b6dcb19d71e5c289781a363e45f8c888c7f6
View Raw JSON Data
{
  "trx_id": "7524b6dcb19d71e5c289781a363e45f8c888c7f6",
  "block": 29201399,
  "trx_in_block": 0,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2019-01-05T22:40:15",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "formatting",
      "author": "typenil",
      "permlink": "git-hooks-autoformat-before-commit",
      "title": "Git Hooks: Autoformat before commit",
      "body": "Autoformatters can be great, keeping diffs small and a code base readable across teams and engineers. [Black](https://github.com/ambv/black) formatting has been enforced in [Lobit](https://www.lobit.io/) builds since day one and I recently added it to the repos at [Downstream](https://www.downstreamimpact.com/) as well.\n\nBut what isn't great is forgetting to run the formatter, having the CI build fail, and having to add a \"Formatting\" commit. That's where [Git Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) come in.\n\nGit hooks are stored in `.git/hooks` folder of any given Git project. There should be some samples pre-populated. For our purposes, `pre-commit` is the hook of interest.\n\nLets focus on the Black formatter for Python as an example. For the example, it needs to be installed locally (`pip install black`). Naively, we can add formatting on pre-commit by adding a file called `pre-commit` to `.git/hooks` with these contents:\n```\nblack .\n```\n(For Javascript, you might have something like `prettier --write **/*.js`)\n\nDon't forget to make the file executable (use `chmod +x .git/hooks/pre-commit`), or the hook may not fire.\n\nIt's nice and simple, but there are a couple issues with this approach. First off, Black will be running over all the source files rather than just the ones that changed (which can be quite slow, depending on the size of your project). Secondly, the modified source files won't actually be added to the commit, which means you need to re-commit the reformatted files.\n\nHere comes the convoluted piping. To get the names and statuses of files that are staged for commit, we can use `git diff --cached --name-status` to yield output like this:\n\n```\n> $ git diff --cached --name-status\n\nM       someproject/src/whatevs.py\nA       someproject/tests/whatevs_test.py\nD       someproject/tests/lasers.py\nM       someproject/README.md\n```\n--------------\n\nWe want to ignore deleted files, since we can't format them, so let's filter them out:\n\n```\n> $ git diff --cached --name-status | grep -v '^D'\n\nM       someproject/src/whatevs.py\nA       someproject/tests/whatevs_test.py\nM       someproject/README.md\n```\n--------------\nWe want to ignore non-Python files (or Javascript or Golang or whatever you're using), so let's filter on file extension:\n\n```\n> $ git diff --cached --name-status | grep -v '^D' | grep '.py'\n\nM       someproject/src/whatevs.py\nA       someproject/tests/whatevs_test.py\n```\n--------------\nNow that we have the files we want, we just want to focus on the names:\n```\n> $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \\t]*//'\n\nsomeproject/src/whatevs.py\nsomeproject/tests/whatevs_test.py\n```\n--------------\nAnd we can pass that right into Black (or whatever formatter you're using):\n```\n> $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \\t]*//' | xargs black\n\nreformatted someproject/src/whatevs.py\nreformatted someproject/tests/whatevs_test.py\nAll done! ✨ 🍰 ✨\n2 files reformatted.\n```\n--------------\nSo that's all great, but the changes still aren't staged. Let's fix that by processing the output from Black into just the files that were reformatted:\n```\n> $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \\t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \\t]//'\n\nsomeproject/src/whatevs.py\nsomeproject/tests/whatevs_test.py\n```\n--------------\nAnd now we can finally stage the formatted files:\n\n```\n> $ git diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \\t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \\t]//' | xargs git add\n```\n\n--------------\n\nSo the final `.git/hooks/pre-commit` file should look something like [this](https://gist.github.com/typenil/14eb5fbd68798bc1aa8179d96d1cc378):\n\n```\ngit diff --cached --name-status | grep -v '^D' | grep '.py' | sed 's/[A-Z][ \\t]*//' | xargs black 2>&1 | grep '^reformatted' | sed 's/reformatted[ \\t]//' | xargs git add\n```\n--------------\nSet it and forget it. Happy coding.\n\n--------------\n*Prefer to catch my posts elsewhere?*\nOrginally published on my blog: [https://typenil.com/git-hooks-autoformat-before-commit/](https://typenil.com/git-hooks-autoformat-before-commit/)",
      "json_metadata": "{\"tags\":[\"formatting\",\"code-quality\",\"git\",\"python\",\"hooks\"],\"links\":[\"https://github.com/ambv/black\",\"https://www.lobit.io/\",\"https://www.downstreamimpact.com/\",\"https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks\",\"https://gist.github.com/typenil/14eb5fbd68798bc1aa8179d96d1cc378\",\"https://typenil.com/git-hooks-autoformat-before-commit/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 6.081 SP to @typenil
2018/10/09 23:11:12
delegatorsteem
delegateetypenil
vesting shares9891.684275 VESTS
Transaction InfoBlock #26669330/Trx ecf708160bdbae222e833cbc6cdfe584afe42fb2
View Raw JSON Data
{
  "trx_id": "ecf708160bdbae222e833cbc6cdfe584afe42fb2",
  "block": 26669330,
  "trx_in_block": 6,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-10-09T23:11:12",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9891.684275 VESTS"
    }
  ]
}
2018/07/17 21:30:36
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
sbd payout0.000 SBD
steem payout0.000 STEEM
vesting payout121.696052 VESTS
Transaction InfoBlock #24265590/Virtual Operation #18
View Raw JSON Data
{
  "trx_id": "0000000000000000000000000000000000000000",
  "block": 24265590,
  "trx_in_block": 4294967295,
  "op_in_trx": 0,
  "virtual_op": 18,
  "timestamp": "2018-07-17T21:30:36",
  "op": [
    "author_reward",
    {
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "sbd_payout": "0.000 SBD",
      "steem_payout": "0.000 STEEM",
      "vesting_payout": "121.696052 VESTS"
    }
  ]
}
2018/07/10 22:01:00
voterhr1
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
weight2 (0.02%)
Transaction InfoBlock #24064691/Trx 5b06f8e896d4c514893de7ad2f8bd2c6444ca59a
View Raw JSON Data
{
  "trx_id": "5b06f8e896d4c514893de7ad2f8bd2c6444ca59a",
  "block": 24064691,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-10T22:01:00",
  "op": [
    "vote",
    {
      "voter": "hr1",
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "weight": 2
    }
  ]
}
2018/07/10 21:30:36
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
max accepted payout1000000.000 SBD
percent steem dollars0
allow votestrue
allow curation rewardstrue
extensions[]
Transaction InfoBlock #24064083/Trx 65557d391a2bcb10a171e7a9dd28011c208b27be
View Raw JSON Data
{
  "trx_id": "65557d391a2bcb10a171e7a9dd28011c208b27be",
  "block": 24064083,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-10T21:30:36",
  "op": [
    "comment_options",
    {
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "max_accepted_payout": "1000000.000 SBD",
      "percent_steem_dollars": 0,
      "allow_votes": true,
      "allow_curation_rewards": true,
      "extensions": []
    }
  ]
}
2018/07/10 21:30:36
parent author
parent permlinkpython
authortypenil
permlinkhow-to-automate-ghost-medium-cross-posting-via-zapier
titleHow to automate Ghost/Medium cross-posting via Zapier
body<html> <div class="kg-card-markdown"><p>With this blog, I'm eager to automate a lot of the tedium that comes with cross-posting to other platforms. Medium was first on the docket due to its popularity - and I quickly came across <a href="https://cmichel.io/how-to-crosspost-to-medium">this fantastic article</a> by Christoph Michel on how he automates the process.</p> <p>(NOTE: automatic Steemit crossposting is next on the docket)</p> <p>At the time of writing, the <a href="http://typenil.com/">typenil</a> blog uses <a href="https://ghost.org/">Ghost</a> for publishing content, so I had to do things a little differently than Christoph. I also wanted to sidestep the need to host the automation code, myself, so using Ghost's built-in <a href="https://zapier.com">Zapier</a> integration made the most sense.</p> <p>(NOTE: you can use the repo <a href="https://github.com/typenil/ghost-crosspost-medium">ghost-crosspost-medium<br> </a> without Ghost or Zapier, but it was originally written with these in mind)</p> <p>Before we get started on anything, we'll need an integration token from Medium. Make sure you're logged into Medium and go to the "Integration tokens" section of <a href="https://medium.com/me/settings">your settings</a>.</p> <p><img src="https://typenil.com/content/images/2018/07/medium-app-3.png" alt="medium-app-3"></p> <p>Get that integration token - you'll need it as a parameter as we head over to Zapier. Sign up or login to <a href="https://zapier.com">Zapier</a> and "Make a Zap!"</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-0.png" alt="zapier-0"></p> <p>Select Ghost as your trigger app; you can search for it if it's not right there. If you haven't connected Ghost before, Zapier will walk you through it.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-1.png" alt="zapier-1"></p> <p>After Ghost is connected and selected, select "New Story" as the Ghost trigger.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-2.png" alt="zapier-2"></p> <p>When asked to select the status for the trigger, pick "Published" unless you have reasons to go with something else.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-4.png" alt="zapier-4"></p> <p>Next stage is selecting the action app. We're going with "Code" and "Run Python" (which is, unfortunately, Python 2.7).</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-5.png" alt="zapier-5"></p> <p><img src="https://typenil.com/content/images/2018/07/zapier-6.png" alt="zapier-6"></p> <p>Configuring the template is where the real setup happens. The required fields for my script are "integrationToken", "content", "title", and "canonicalUrl", but including "tags" is also adviseable.</p> <p>"integrationToken" is the token you set up in Medium earlier; it'll be the same for all requests.</p> <p>Each of the other fields correspond to values provided by Ghost. This will be easiest if you already have at least 1 post up (even if you post it just for this exercise) since Zapier will show you an example of the input.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-7.png" alt="zapier-7"></p> <table> <thead> <tr> <th>Input Name</th> <th>Ghost Name</th> </tr> </thead> <tbody> <tr> <td>title</td> <td>Title</td> </tr> <tr> <td>canonicalUrl</td> <td>URL</td> </tr> <tr> <td>content</td> <td>HTML Formatted Content</td> </tr> <tr> <td>tags</td> <td>Tags Slug</td> </tr> </tbody> </table> <p>The order you add the items in doesn't matter - and if you're a more advanced user, any additional fields you specify will also be passed on to the Medium call (see the <a href="https://github.com/Medium/medium-api-docs#creating-a-post">Medium post API docs</a> for more info).</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-10.png" alt="zapier-10"></p> <p>Once the inputs are set up, it's time for the code, itself. I've set up a repository for this project on my GitHub under <a href="https://github.com/typenil/ghost-crosspost-medium">ghost-crosspost-medium</a>, but to get this working on Zapier you just have to copy and paste <a href="https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py">this file</a>.</p> <p>(NOTE: Omitting the code here, as I can't get Steemit to format it correctly.)</p> <hr> <p>Take the code from the link above and paste it into the "Code" section at the bottom of the Zapier template page.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-11.png" alt="zapier-11"></p> <p>By this point, you should be good to wrap it up. Scroll to the bottom and continue. You'll then have the opportunity to test the connection.</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-12.png" alt="zapier-12"><br> <img src="https://typenil.com/content/images/2018/07/zapier-13.png" alt="zapier-13"></p> <p>If everything checks out, click "Finish" and turn on your Zap!</p> <p><img src="https://typenil.com/content/images/2018/07/zapier-14.png" alt="zapier-14"></p> <p>If you're having problems, feel free to open an issue on <a href="https://github.com/typenil/ghost-crosspost-medium">that GitHub repo</a>. The error messages returned from the Medium API are incredibly vague, so debugging it can be a major pain.</p> <p>In the future, I'd like to add automatic relative link resolution like Christoph includes in <a href="https://cmichel.io/how-to-crosspost-to-medium/">his tutorial</a>. For Ghost, this doesn't appear to be necessary; relative links get resolved by the time they hit Zapier.</p> <hr> <p><em>Prefer to catch my posts elsewhere?</em></p> <ul> <li>Typenil Blog: <a href="https://typenil.com/">https://typenil.com/</a></li> <li>Medium: <a href="https://medium.com/@typenil">https://medium.com/@typenil</a></li> </ul> Originally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/ </div> </html>
json metadata{"tags":["python","ghost","medium","blogging","zapier"],"image":["https://typenil.com/content/images/2018/07/medium-app-3.png","https://typenil.com/content/images/2018/07/zapier-0.png","https://typenil.com/content/images/2018/07/zapier-1.png","https://typenil.com/content/images/2018/07/zapier-2.png","https://typenil.com/content/images/2018/07/zapier-4.png","https://typenil.com/content/images/2018/07/zapier-5.png","https://typenil.com/content/images/2018/07/zapier-6.png","https://typenil.com/content/images/2018/07/zapier-7.png","https://typenil.com/content/images/2018/07/zapier-10.png","https://typenil.com/content/images/2018/07/zapier-11.png","https://typenil.com/content/images/2018/07/zapier-12.png","https://typenil.com/content/images/2018/07/zapier-13.png","https://typenil.com/content/images/2018/07/zapier-14.png"],"links":["https://cmichel.io/how-to-crosspost-to-medium","http://typenil.com/","https://ghost.org/","https://zapier.com","https://github.com/typenil/ghost-crosspost-medium","https://medium.com/me/settings","https://github.com/Medium/medium-api-docs#creating-a-post","https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py","https://cmichel.io/how-to-crosspost-to-medium/","https://typenil.com/","https://medium.com/@typenil","https://typenil.com/automatic-ghost-medium-cross-posting/"],"app":"steemit/0.1","format":"html"}
Transaction InfoBlock #24064083/Trx 65557d391a2bcb10a171e7a9dd28011c208b27be
View Raw JSON Data
{
  "trx_id": "65557d391a2bcb10a171e7a9dd28011c208b27be",
  "block": 24064083,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-10T21:30:36",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "how-to-automate-ghost-medium-cross-posting-via-zapier",
      "title": "How to automate Ghost/Medium cross-posting via Zapier",
      "body": "<html>\n<div class=\"kg-card-markdown\"><p>With this blog, I'm eager to automate a lot of the tedium that comes with cross-posting to other platforms. Medium was first on the docket due to its popularity - and I quickly came across <a href=\"https://cmichel.io/how-to-crosspost-to-medium\">this fantastic article</a> by Christoph Michel on how he automates the process.</p>\n<p>(NOTE: automatic Steemit crossposting is next on the docket)</p>\n<p>At the time of writing, the <a href=\"http://typenil.com/\">typenil</a> blog uses <a href=\"https://ghost.org/\">Ghost</a> for publishing content, so I had to do things a little differently than Christoph. I also wanted to sidestep the need to host the automation code, myself, so using Ghost's built-in <a href=\"https://zapier.com\">Zapier</a> integration made the most sense.</p>\n<p>(NOTE: you can use the repo <a href=\"https://github.com/typenil/ghost-crosspost-medium\">ghost-crosspost-medium<br>\n</a> without Ghost or Zapier, but it was originally written with these in mind)</p>\n<p>Before we get started on anything, we'll need an integration token from Medium. Make sure you're logged into Medium and go to the \"Integration tokens\" section of <a href=\"https://medium.com/me/settings\">your settings</a>.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/medium-app-3.png\" alt=\"medium-app-3\"></p>\n<p>Get that integration token - you'll need it as a parameter as we head over to Zapier. Sign up or login to <a href=\"https://zapier.com\">Zapier</a> and \"Make a Zap!\"</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-0.png\" alt=\"zapier-0\"></p>\n<p>Select Ghost as your trigger app; you can search for it if it's not right there. If you haven't connected Ghost before, Zapier will walk you through it.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-1.png\" alt=\"zapier-1\"></p>\n<p>After Ghost is connected and selected, select \"New Story\" as the Ghost trigger.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-2.png\" alt=\"zapier-2\"></p>\n<p>When asked to select the status for the trigger, pick \"Published\" unless you have reasons to go with something else.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-4.png\" alt=\"zapier-4\"></p>\n<p>Next stage is selecting the action app. We're going with \"Code\" and \"Run Python\" (which is, unfortunately, Python 2.7).</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-5.png\" alt=\"zapier-5\"></p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-6.png\" alt=\"zapier-6\"></p>\n<p>Configuring the template is where the real setup happens. The required fields for my script are \"integrationToken\", \"content\", \"title\", and \"canonicalUrl\", but including \"tags\" is also adviseable.</p>\n<p>\"integrationToken\" is the token you set up in Medium earlier; it'll be the same for all requests.</p>\n<p>Each of the other fields correspond to values provided by Ghost. This will be easiest if you already have at least 1 post up (even if you post it just for this exercise) since Zapier will show you an example of the input.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-7.png\" alt=\"zapier-7\"></p>\n<table>\n<thead>\n<tr>\n<th>Input Name</th>\n<th>Ghost Name</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>title</td>\n<td>Title</td>\n</tr>\n<tr>\n<td>canonicalUrl</td>\n<td>URL</td>\n</tr>\n<tr>\n<td>content</td>\n<td>HTML Formatted Content</td>\n</tr>\n<tr>\n<td>tags</td>\n<td>Tags Slug</td>\n</tr>\n</tbody>\n</table>\n<p>The order you add the items in doesn't matter - and if you're a more advanced user, any additional fields you specify will also be passed on to the Medium call (see the <a href=\"https://github.com/Medium/medium-api-docs#creating-a-post\">Medium post API docs</a> for more info).</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-10.png\" alt=\"zapier-10\"></p>\n<p>Once the inputs are set up, it's time for the code, itself. I've set up a repository for this project on my GitHub under <a href=\"https://github.com/typenil/ghost-crosspost-medium\">ghost-crosspost-medium</a>, but to get this working on Zapier you just have to copy and paste <a href=\"https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py\">this file</a>.</p>\n\n<p>(NOTE: Omitting the code here, as I can't get Steemit to format it correctly.)</p>\n\n<hr>\n<p>Take the code from the link above and paste it into the \"Code\" section at the bottom of the Zapier template page.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-11.png\" alt=\"zapier-11\"></p>\n<p>By this point, you should be good to wrap it up. Scroll to the bottom and continue. You'll then have the opportunity to test the connection.</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-12.png\" alt=\"zapier-12\"><br>\n<img src=\"https://typenil.com/content/images/2018/07/zapier-13.png\" alt=\"zapier-13\"></p>\n<p>If everything checks out, click \"Finish\" and turn on your Zap!</p>\n<p><img src=\"https://typenil.com/content/images/2018/07/zapier-14.png\" alt=\"zapier-14\"></p>\n<p>If you're having problems, feel free to open an issue on <a href=\"https://github.com/typenil/ghost-crosspost-medium\">that GitHub repo</a>. The error messages returned from the Medium API are incredibly vague, so debugging it can be a major pain.</p>\n<p>In the future, I'd like to add automatic relative link resolution like Christoph includes in <a href=\"https://cmichel.io/how-to-crosspost-to-medium/\">his tutorial</a>. For Ghost, this doesn't appear to be necessary; relative links get resolved by the time they hit Zapier.</p>\n<hr>\n<p><em>Prefer to catch my posts elsewhere?</em></p>\n<ul>\n<li>Typenil Blog: <a href=\"https://typenil.com/\">https://typenil.com/</a></li>\n<li>Medium: <a href=\"https://medium.com/@typenil\">https://medium.com/@typenil</a></li>\n</ul>\n\nOriginally published on my blog: https://typenil.com/automatic-ghost-medium-cross-posting/\n</div>\n</html>",
      "json_metadata": "{\"tags\":[\"python\",\"ghost\",\"medium\",\"blogging\",\"zapier\"],\"image\":[\"https://typenil.com/content/images/2018/07/medium-app-3.png\",\"https://typenil.com/content/images/2018/07/zapier-0.png\",\"https://typenil.com/content/images/2018/07/zapier-1.png\",\"https://typenil.com/content/images/2018/07/zapier-2.png\",\"https://typenil.com/content/images/2018/07/zapier-4.png\",\"https://typenil.com/content/images/2018/07/zapier-5.png\",\"https://typenil.com/content/images/2018/07/zapier-6.png\",\"https://typenil.com/content/images/2018/07/zapier-7.png\",\"https://typenil.com/content/images/2018/07/zapier-10.png\",\"https://typenil.com/content/images/2018/07/zapier-11.png\",\"https://typenil.com/content/images/2018/07/zapier-12.png\",\"https://typenil.com/content/images/2018/07/zapier-13.png\",\"https://typenil.com/content/images/2018/07/zapier-14.png\"],\"links\":[\"https://cmichel.io/how-to-crosspost-to-medium\",\"http://typenil.com/\",\"https://ghost.org/\",\"https://zapier.com\",\"https://github.com/typenil/ghost-crosspost-medium\",\"https://medium.com/me/settings\",\"https://github.com/Medium/medium-api-docs#creating-a-post\",\"https://github.com/typenil/ghost-crosspost-medium/blob/master/medium_crosspost/medium_crosspost.py\",\"https://cmichel.io/how-to-crosspost-to-medium/\",\"https://typenil.com/\",\"https://medium.com/@typenil\",\"https://typenil.com/automatic-ghost-medium-cross-posting/\"],\"app\":\"steemit/0.1\",\"format\":\"html\"}"
    }
  ]
}
2018/07/01 20:18:27
required auths[]
required posting auths["typenil"]
idfollow
json["follow",{"follower":"typenil","following":"cmichel","what":["blog"]}]
Transaction InfoBlock #23813385/Trx 2d6b86b91713a45f6f080d2268a8d463de8554cb
View Raw JSON Data
{
  "trx_id": "2d6b86b91713a45f6f080d2268a8d463de8554cb",
  "block": 23813385,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T20:18:27",
  "op": [
    "custom_json",
    {
      "required_auths": [],
      "required_posting_auths": [
        "typenil"
      ],
      "id": "follow",
      "json": "[\"follow\",{\"follower\":\"typenil\",\"following\":\"cmichel\",\"what\":[\"blog\"]}]"
    }
  ]
}
typenilupdated their account properties
2018/07/01 19:47:06
accounttypenil
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{"profile":{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Data processing specialist. Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js. Cryptocurrency weirdo.","website":"http://typenil.com/"}}
Transaction InfoBlock #23812758/Trx a78676dab12ef6e80f7950ffae3c374f0ff652aa
View Raw JSON Data
{
  "trx_id": "a78676dab12ef6e80f7950ffae3c374f0ff652aa",
  "block": 23812758,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T19:47:06",
  "op": [
    "account_update",
    {
      "account": "typenil",
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Data processing specialist.  Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js.  Cryptocurrency weirdo.\",\"website\":\"http://typenil.com/\"}}"
    }
  ]
}
typenilupdated their account properties
2018/07/01 19:39:39
accounttypenil
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{"profile":{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmRMnixXMQ6LCepY2zq6uZ49H5t5py65utJ1zoekb2biT8/social%20media_facebook%20cover%20black.png","about":"Data processing specialist. Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js. Cryptocurrency weirdo.","website":"http://typenil.com/"}}
Transaction InfoBlock #23812609/Trx 66ae7fb0eb0c30b9ee77ced3815da862831760d9
View Raw JSON Data
{
  "trx_id": "66ae7fb0eb0c30b9ee77ced3815da862831760d9",
  "block": 23812609,
  "trx_in_block": 11,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T19:39:39",
  "op": [
    "account_update",
    {
      "account": "typenil",
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmRMnixXMQ6LCepY2zq6uZ49H5t5py65utJ1zoekb2biT8/social%20media_facebook%20cover%20black.png\",\"about\":\"Data processing specialist.  Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js.  Cryptocurrency weirdo.\",\"website\":\"http://typenil.com/\"}}"
    }
  ]
}
2018/07/01 02:32:30
votermagpielover
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
weight10000 (100.00%)
Transaction InfoBlock #23792068/Trx 49f2d175edd47b88a6e72df6f88932636fc61cdc
View Raw JSON Data
{
  "trx_id": "49f2d175edd47b88a6e72df6f88932636fc61cdc",
  "block": 23792068,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T02:32:30",
  "op": [
    "vote",
    {
      "voter": "magpielover",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "weight": 10000
    }
  ]
}
2018/07/01 02:26:42
parent authortypenil
parent permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
authorintroduce.bot
permlinkintroduce-bot-re-typenilsqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
title
body✅ @typenil, I gave you an upvote on your post! **Please give me a follow** and I will give you a follow in return and possible future votes!<br><br>Thank you in advance!
json metadata
Transaction InfoBlock #23791952/Trx cf70c5077884b18a90112da64693505c4b2f248d
View Raw JSON Data
{
  "trx_id": "cf70c5077884b18a90112da64693505c4b2f248d",
  "block": 23791952,
  "trx_in_block": 17,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T02:26:42",
  "op": [
    "comment",
    {
      "parent_author": "typenil",
      "parent_permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "author": "introduce.bot",
      "permlink": "introduce-bot-re-typenilsqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "title": "",
      "body": "✅  @typenil, I gave you an upvote on your post! **Please give me a follow** and I will give you a follow in return and possible future votes!<br><br>Thank you in advance!",
      "json_metadata": ""
    }
  ]
}
2018/07/01 02:26:42
voterintroduce.bot
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
weight38 (0.38%)
Transaction InfoBlock #23791952/Trx 615ff9d464e1f0730ab12c0a734dc6e11caaf0fc
View Raw JSON Data
{
  "trx_id": "615ff9d464e1f0730ab12c0a734dc6e11caaf0fc",
  "block": 23791952,
  "trx_in_block": 4,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T02:26:42",
  "op": [
    "vote",
    {
      "voter": "introduce.bot",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "weight": 38
    }
  ]
}
2018/07/01 01:59:24
voteralphabot
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
weight100 (1.00%)
Transaction InfoBlock #23791406/Trx 5caba3441c619ea33bace69cbd368bd830cfc58c
View Raw JSON Data
{
  "trx_id": "5caba3441c619ea33bace69cbd368bd830cfc58c",
  "block": 23791406,
  "trx_in_block": 13,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T01:59:24",
  "op": [
    "vote",
    {
      "voter": "alphabot",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "weight": 100
    }
  ]
}
2018/07/01 01:59:09
parent author
parent permlinkpython
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
titleSQLAlchemy + FactoryBoy: Passing arbitrary sessions to factories
body@@ -995,16 +995,32 @@ plate:%0A%0A +--------------%0A%0A %60%60%60%0Afrom @@ -1656,16 +1656,32 @@ ()%0A%60%60%60%0A%0A +--------------%0A%0A # Fixing @@ -1945,16 +1945,32 @@ , e.g.%0A%0A +--------------%0A%0A %60%60%60%0Aimpo @@ -2508,16 +2508,32 @@ trick:%0A%0A +--------------%0A%0A %0A%60%60%60%0Aimp @@ -3087,16 +3087,32 @@ ry%0A%60%60%60%0A%0A +--------------%0A%0A While si @@ -3628,16 +3628,32 @@ ssion:%0A%0A +--------------%0A%0A %60%60%60%0A @@ -3858,16 +3858,32 @@ ..%0A%60%60%60%0A%0A +--------------%0A%0A # Cleani @@ -4172,16 +4172,32 @@ othly.%0A%0A +--------------%0A%0A %60%60%60%0Afrom @@ -4626,16 +4626,32 @@ ))%0A%60%60%60%0A%0A +--------------%0A%0A The Fact @@ -4826,16 +4826,32 @@ e one.%0A%0A +--------------%0A%0A %60%60%60%0A @@ -4967,21 +4967,37 @@ ession)%0A - %60%60%60%0A%0A +--------------%0A%0A Now I co
json metadata{"tags":["python","sqlalchemy","factoryboy","sessions","testing"],"image":["https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2"],"links":["http://www.lobit.io/","http://typenil.com/sqlalchemy-session-wrapping-factories/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #23791401/Trx 0a4f0d94ba03b8c1e53c07a29e6200ebb71330c7
View Raw JSON Data
{
  "trx_id": "0a4f0d94ba03b8c1e53c07a29e6200ebb71330c7",
  "block": 23791401,
  "trx_in_block": 27,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T01:59:09",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "title": "SQLAlchemy + FactoryBoy: Passing arbitrary sessions to factories",
      "body": "@@ -995,16 +995,32 @@\n plate:%0A%0A\n+--------------%0A%0A\n %60%60%60%0Afrom\n@@ -1656,16 +1656,32 @@\n ()%0A%60%60%60%0A%0A\n+--------------%0A%0A\n # Fixing\n@@ -1945,16 +1945,32 @@\n , e.g.%0A%0A\n+--------------%0A%0A\n %60%60%60%0Aimpo\n@@ -2508,16 +2508,32 @@\n trick:%0A%0A\n+--------------%0A%0A\n %0A%60%60%60%0Aimp\n@@ -3087,16 +3087,32 @@\n ry%0A%60%60%60%0A%0A\n+--------------%0A%0A\n While si\n@@ -3628,16 +3628,32 @@\n ssion:%0A%0A\n+--------------%0A%0A\n %60%60%60%0A    \n@@ -3858,16 +3858,32 @@\n ..%0A%60%60%60%0A%0A\n+--------------%0A%0A\n # Cleani\n@@ -4172,16 +4172,32 @@\n othly.%0A%0A\n+--------------%0A%0A\n %60%60%60%0Afrom\n@@ -4626,16 +4626,32 @@\n ))%0A%60%60%60%0A%0A\n+--------------%0A%0A\n The Fact\n@@ -4826,16 +4826,32 @@\n e one.%0A%0A\n+--------------%0A%0A\n %60%60%60%0A    \n@@ -4967,21 +4967,37 @@\n ession)%0A\n-\n %60%60%60%0A%0A\n+--------------%0A%0A\n Now I co\n",
      "json_metadata": "{\"tags\":[\"python\",\"sqlalchemy\",\"factoryboy\",\"sessions\",\"testing\"],\"image\":[\"https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2\"],\"links\":[\"http://www.lobit.io/\",\"http://typenil.com/sqlalchemy-session-wrapping-factories/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
2018/07/01 01:56:45
parent authortypenil
parent permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
authorallnatural
permlinkre-typenil-sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories-20180701t015648438z
title
body# # upvote for me please? https://steemit.com/news/@bible.com/6h36cq #
json metadata{"tags":["python"],"links":["https://steemit.com/news/@bible.com/6h36cq"],"app":"steemit/0.1"}
Transaction InfoBlock #23791353/Trx 0dc4da03a0cf014d9436338ec73f596e1c5cfd9a
View Raw JSON Data
{
  "trx_id": "0dc4da03a0cf014d9436338ec73f596e1c5cfd9a",
  "block": 23791353,
  "trx_in_block": 30,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T01:56:45",
  "op": [
    "comment",
    {
      "parent_author": "typenil",
      "parent_permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "author": "allnatural",
      "permlink": "re-typenil-sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories-20180701t015648438z",
      "title": "",
      "body": "#\n# upvote for me please? https://steemit.com/news/@bible.com/6h36cq\n#",
      "json_metadata": "{\"tags\":[\"python\"],\"links\":[\"https://steemit.com/news/@bible.com/6h36cq\"],\"app\":\"steemit/0.1\"}"
    }
  ]
}
2018/07/01 01:56:42
voterax3
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
weight100 (1.00%)
Transaction InfoBlock #23791352/Trx a8e56821e14cd2b200f4d383a7131d2f0f043bd2
View Raw JSON Data
{
  "trx_id": "a8e56821e14cd2b200f4d383a7131d2f0f043bd2",
  "block": 23791352,
  "trx_in_block": 3,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T01:56:42",
  "op": [
    "vote",
    {
      "voter": "ax3",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "weight": 100
    }
  ]
}
2018/07/01 01:56:30
parent author
parent permlinkpython
authortypenil
permlinksqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
titleSQLAlchemy + FactoryBoy: Passing arbitrary sessions to factories
bodyIn work projects in the past, my team would try to avoid dealing with the complexity of SQLAlchemy database sessions by making one global session that every module referenced. It made things easy and straightforward and it played nice with FactoryBoy - whose factories seem to work quite well under those conditions. !["Don't Look At Me" - Patrick Hendry](https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2) Going into building [lobit.io](http://www.lobit.io/), I tried to do things the same way, but as the code base and the number of unit tests grew, I kept facing what seemed to be an unquashable avalanche of OperationalErrors griping about "Too many connections". I implemented an overhaul of the codebase, wrapping every piece of ORM code in a separate session and creating a DbUtils class to handle sessions consistently without too much boilerplate: ``` from contextlib import contextmanager from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session from config import config class DbUtils: Session = sessionmaker(bind=create_engine(config.db_connection_url)) @classmethod @contextmanager def session_scope(cls) -> Session: """Provide a transactional scope around a series of operations.""" session = cls.Session() try: yield session session.commit() except: session.rollback() raise finally: session.close() ``` # Fixing the test factories When it came to unit tests, I was at a loss for how to best handle all of the factories I was using. They were all tied into the global session. Furthermore, factories took in the session at the time of class creation, not instantiation, e.g. ``` import factory from models.post import Post from tests.factories.post_thread_factory import PostThreadFactory class PostFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Post sqlalchemy_session_persistence = "commit" sqlalchemy_session = session text = factory.Faker("text", max_nb_chars=100) external_id = factory.Faker("text", max_nb_chars=50) post_thread = factory.SubFactory(PostThreadFactory) ``` -------------- Wrapping factory class creation in a function did the trick: ``` import factory from models.post import Post from tests.factories.post_thread_factory import PostThreadFactory def PostFactory(session): class _PostFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = Post sqlalchemy_session_persistence = "commit" sqlalchemy_session = session text = factory.Faker("text", max_nb_chars=100) external_id = factory.Faker("text", max_nb_chars=50) post_thread = factory.SubFactory(PostThreadFactory(session)) return _PostFactory ``` While simple wrapping worked, it made my tests look God-awful. Manually managing sessions had already added boilerplate overhead to every unit test that hit the database. Before refactoring, I could create a post using `PostFactory.create()`. Now it was `PostFactory(session).create()`. Most of the unit tests hit multiple factories several times each, so it was also less efficient to keep creating the same classes over and over. I took to creating a single instance of each required factory at the top of each session: ``` def test_post_stuff(self): with DbUtils.session_scope() as session: post_factory = PostFactory(session) post_thread_factory = PostThreadFactory(session) ... ``` # Cleaning it up A given test could have nearly a dozen factories defined at the top - which looked terrible even if it didn't trigger a "too-many-locals" warning from Pylint (which it almost always did). So I made one more change and hired a manager to keep all the factories running smoothly. ``` from tests.factories.post_factory import PostFactory from tests.factories.post_thread_factory import PostThreadFactory ... class FactoryManager: known_factories = [ PostFactory, PostThreadFactory, ... ] def __init__(self, session): self.session = session for factory_func in self.known_factories: setattr(self, factory_func.__name__, factory_func(self.session)) ``` The FactoryManager didn't require a lot of code, but let me leave my tests largely unchanged. Instead of initializing several objects at the top of each test, I could initialize one. ``` def test_post_stuff(self): with DbUtils.session_scope() as session: fm = FactoryManager(session) ``` Now I could prefix all factory calls with `fm.` and get something *almost* as clean as before. At least, `fm.PostFactory.create()` looks better to me than `PostFactory(session).create()` or littering each test with a bunch of locals. ----------- *Prefer to catch my posts elsewhere?* Originally published on my blog: http://typenil.com/sqlalchemy-session-wrapping-factories/
json metadata{"tags":["python","sqlalchemy","factoryboy","sessions","testing"],"image":["https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2"],"links":["http://www.lobit.io/","http://typenil.com/sqlalchemy-session-wrapping-factories/"],"app":"steemit/0.1","format":"markdown"}
Transaction InfoBlock #23791348/Trx 36b870ac8982e76b7229a84d94a4ace1fb47856c
View Raw JSON Data
{
  "trx_id": "36b870ac8982e76b7229a84d94a4ace1fb47856c",
  "block": 23791348,
  "trx_in_block": 50,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-07-01T01:56:30",
  "op": [
    "comment",
    {
      "parent_author": "",
      "parent_permlink": "python",
      "author": "typenil",
      "permlink": "sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories",
      "title": "SQLAlchemy + FactoryBoy: Passing arbitrary sessions to factories",
      "body": "In work projects in the past, my team would try to avoid dealing with the complexity of SQLAlchemy database sessions by making one global session that every module referenced. It made things easy and straightforward and it played nice with FactoryBoy - whose factories seem to work quite well under those conditions.\n\n![\"Don't Look At Me\" - Patrick Hendry](https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2)\n\nGoing into building [lobit.io](http://www.lobit.io/), I tried to do things the same way, but as the code base and the number of unit tests grew, I kept facing what seemed to be an unquashable avalanche of OperationalErrors griping about \"Too many connections\".\n\nI implemented an overhaul of the codebase, wrapping every piece of ORM code in a separate session and creating a DbUtils class to handle sessions consistently without too much boilerplate:\n\n```\nfrom contextlib import contextmanager\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import sessionmaker\nfrom sqlalchemy.orm.session import Session\nfrom config import config\n\n\nclass DbUtils:\n    Session = sessionmaker(bind=create_engine(config.db_connection_url))\n\n    @classmethod\n    @contextmanager\n    def session_scope(cls) -> Session:\n        \"\"\"Provide a transactional scope around a series of operations.\"\"\"\n        session = cls.Session()\n        try:\n            yield session\n            session.commit()\n        except:\n            session.rollback()\n            raise\n        finally:\n            session.close()\n```\n\n# Fixing the test factories\n\nWhen it came to unit tests, I was at a loss for how to best handle all of the factories I was using. They were all tied into the global session. Furthermore, factories took in the session at the time of class creation, not instantiation, e.g.\n\n```\nimport factory\nfrom models.post import Post\nfrom tests.factories.post_thread_factory import PostThreadFactory\n\n\nclass PostFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = Post\n        sqlalchemy_session_persistence = \"commit\"\n        sqlalchemy_session = session\n\n    text = factory.Faker(\"text\", max_nb_chars=100)\n    external_id = factory.Faker(\"text\", max_nb_chars=50)\n    post_thread = factory.SubFactory(PostThreadFactory)\n```\n\n--------------\n\nWrapping factory class creation in a function did the trick:\n\n\n```\nimport factory\nfrom models.post import Post\nfrom tests.factories.post_thread_factory import PostThreadFactory\n\n\ndef PostFactory(session):\n    class _PostFactory(factory.alchemy.SQLAlchemyModelFactory):\n        class Meta:\n            model = Post\n            sqlalchemy_session_persistence = \"commit\"\n            sqlalchemy_session = session\n\n        text = factory.Faker(\"text\", max_nb_chars=100)\n        external_id = factory.Faker(\"text\", max_nb_chars=50)\n        post_thread = factory.SubFactory(PostThreadFactory(session))\n\n    return _PostFactory\n```\n\nWhile simple wrapping worked, it made my tests look God-awful. Manually managing sessions had already added boilerplate overhead to every unit test that hit the database.\n\nBefore refactoring, I could create a post using `PostFactory.create()`. Now it was `PostFactory(session).create()`. Most of the unit tests hit multiple factories several times each, so it was also less efficient to keep creating the same classes over and over.\n\nI took to creating a single instance of each required factory at the top of each session:\n\n```\n    def test_post_stuff(self):\n        with DbUtils.session_scope() as session:\n            post_factory = PostFactory(session)\n            post_thread_factory = PostThreadFactory(session)\n            ...\n```\n\n# Cleaning it up\n\nA given test could have nearly a dozen factories defined at the top - which looked terrible even if it didn't trigger a \"too-many-locals\" warning from Pylint (which it almost always did).\n\nSo I made one more change and hired a manager to keep all the factories running smoothly.\n\n```\nfrom tests.factories.post_factory import PostFactory\nfrom tests.factories.post_thread_factory import PostThreadFactory\n...\n\nclass FactoryManager:\n    known_factories = [\n        PostFactory,\n        PostThreadFactory,\n        ...\n    ]\n\n    def __init__(self, session):\n        self.session = session\n\n        for factory_func in self.known_factories:\n            setattr(self, factory_func.__name__, factory_func(self.session))\n```\n\nThe FactoryManager didn't require a lot of code, but let me leave my tests largely unchanged. Instead of initializing several objects at the top of each test, I could initialize one.\n\n```\n    def test_post_stuff(self):\n        with DbUtils.session_scope() as session:\n            fm = FactoryManager(session)\n```\n\nNow I could prefix all factory calls with `fm.` and get something *almost* as clean as before. At least, `fm.PostFactory.create()` looks better to me than `PostFactory(session).create()` or littering each test with a bunch of locals.\n\n-----------\n*Prefer to catch my posts elsewhere?*\nOriginally published on my blog: http://typenil.com/sqlalchemy-session-wrapping-factories/",
      "json_metadata": "{\"tags\":[\"python\",\"sqlalchemy\",\"factoryboy\",\"sessions\",\"testing\"],\"image\":[\"https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ&s=bf504a1d5183840e87511d01ce729be2\"],\"links\":[\"http://www.lobit.io/\",\"http://typenil.com/sqlalchemy-session-wrapping-factories/\"],\"app\":\"steemit/0.1\",\"format\":\"markdown\"}"
    }
  ]
}
steemdelegated 18.597 SP to @typenil
2018/06/28 21:02:06
delegatorsteem
delegateetypenil
vesting shares30249.630303 VESTS
Transaction InfoBlock #23727912/Trx 81024cbacefc70eed1bb5fe79cd53954ad18f1c3
View Raw JSON Data
{
  "trx_id": "81024cbacefc70eed1bb5fe79cd53954ad18f1c3",
  "block": 23727912,
  "trx_in_block": 9,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-06-28T21:02:06",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "30249.630303 VESTS"
    }
  ]
}
typenilupdated their account properties
2018/06/28 19:48:00
accounttypenil
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{"profile":{"profile_image":"https://cdn.steemitimages.com/DQmW86SN1q9VxEcg5kMvriZQDTtgBxtFbfBwDzS9FvJQtnE/social%20media_profile%20picture-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Data processing specialist. Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js. Cryptocurrency weirdo.","website":"http://typenil.com/"}}
Transaction InfoBlock #23726430/Trx ce830531f042cc8e8149e34ecbc6594667d83950
View Raw JSON Data
{
  "trx_id": "ce830531f042cc8e8149e34ecbc6594667d83950",
  "block": 23726430,
  "trx_in_block": 21,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-06-28T19:48:00",
  "op": [
    "account_update",
    {
      "account": "typenil",
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{\"profile\":{\"profile_image\":\"https://cdn.steemitimages.com/DQmW86SN1q9VxEcg5kMvriZQDTtgBxtFbfBwDzS9FvJQtnE/social%20media_profile%20picture-%20black.png\",\"cover_image\":\"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg\",\"about\":\"Data processing specialist.  Full-stack web developer using Python, Flask, SQLAlchemy, and Vue.js.  Cryptocurrency weirdo.\",\"website\":\"http://typenil.com/\"}}"
    }
  ]
}
steemdelegated 6.124 SP to @typenil
2018/06/01 22:18:15
delegatorsteem
delegateetypenil
vesting shares9961.405869 VESTS
Transaction InfoBlock #22952606/Trx e5f5802675dfd005b1053cd9adcd25d9bac9a240
View Raw JSON Data
{
  "trx_id": "e5f5802675dfd005b1053cd9adcd25d9bac9a240",
  "block": 22952606,
  "trx_in_block": 18,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-06-01T22:18:15",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "9961.405869 VESTS"
    }
  ]
}
steemdelegated 18.713 SP to @typenil
2018/03/02 21:04:39
delegatorsteem
delegateetypenil
vesting shares30438.322073 VESTS
Transaction InfoBlock #20333083/Trx 892aa656295d7be100fefbfe09e9158b30d89fc7
View Raw JSON Data
{
  "trx_id": "892aa656295d7be100fefbfe09e9158b30d89fc7",
  "block": 20333083,
  "trx_in_block": 2,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-03-02T21:04:39",
  "op": [
    "delegate_vesting_shares",
    {
      "delegator": "steem",
      "delegatee": "typenil",
      "vesting_shares": "30438.322073 VESTS"
    }
  ]
}
steemcreated a new account: @typenil
2018/03/02 19:34:15
fee0.100 STEEM
delegation30690.000000 VESTS
creatorsteem
new account nametypenil
owner{"weight_threshold":1,"account_auths":[],"key_auths":[["STM5JBNRJKnW9hTWAhoDXppLA1FTnJMV7agze5ye8gd1Zb4ZGqcFK",1]]}
active{"weight_threshold":1,"account_auths":[],"key_auths":[["STM5YAtprKXbKameJcDhnwxELYvTH953VQRCeU6VuvnvEWeCXnor7",1]]}
posting{"weight_threshold":1,"account_auths":[],"key_auths":[["STM6YK6wa24fPaeyVcvS5a2EKarRR7b2RwwHvgo6cDwGPd24sxBD5",1]]}
memo keySTM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
json metadata{}
extensions[]
Transaction InfoBlock #20331275/Trx 2e14fb00832394e6083da0f0c09fcd923f339379
View Raw JSON Data
{
  "trx_id": "2e14fb00832394e6083da0f0c09fcd923f339379",
  "block": 20331275,
  "trx_in_block": 35,
  "op_in_trx": 0,
  "virtual_op": 0,
  "timestamp": "2018-03-02T19:34:15",
  "op": [
    "account_create_with_delegation",
    {
      "fee": "0.100 STEEM",
      "delegation": "30690.000000 VESTS",
      "creator": "steem",
      "new_account_name": "typenil",
      "owner": {
        "weight_threshold": 1,
        "account_auths": [],
        "key_auths": [
          [
            "STM5JBNRJKnW9hTWAhoDXppLA1FTnJMV7agze5ye8gd1Zb4ZGqcFK",
            1
          ]
        ]
      },
      "active": {
        "weight_threshold": 1,
        "account_auths": [],
        "key_auths": [
          [
            "STM5YAtprKXbKameJcDhnwxELYvTH953VQRCeU6VuvnvEWeCXnor7",
            1
          ]
        ]
      },
      "posting": {
        "weight_threshold": 1,
        "account_auths": [],
        "key_auths": [
          [
            "STM6YK6wa24fPaeyVcvS5a2EKarRR7b2RwwHvgo6cDwGPd24sxBD5",
            1
          ]
        ]
      },
      "memo_key": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i",
      "json_metadata": "{}",
      "extensions": []
    }
  ]
}

Account Metadata

POSTING JSON METADATA
profile{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Developer","website":"https://mdub.dev/","name":"Matt White","location":"United States"}
JSON METADATA
profile{"profile_image":"https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png","cover_image":"https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg","about":"Developer","website":"https://typenil.com/","name":"typenil","location":"United States"}
{
  "posting_json_metadata": {
    "profile": {
      "profile_image": "https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png",
      "cover_image": "https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg",
      "about": "Developer",
      "website": "https://mdub.dev/",
      "name": "Matt White",
      "location": "United States"
    }
  },
  "json_metadata": {
    "profile": {
      "profile_image": "https://cdn.steemitimages.com/DQmUackmwg7p66EA7ocJsiXVWmEpxeFAtp5wAXp1jzEsojo/Just%20Logo%20-%20black.png",
      "cover_image": "https://cdn.steemitimages.com/DQmaeCcHMgAyxfygLNxZuQ8K6hwr8MZJSkckN4FAgJvD4uF/pankaj-patel-516482-unsplash%20(copy).jpg",
      "about": "Developer",
      "website": "https://typenil.com/",
      "name": "typenil",
      "location": "United States"
    }
  }
}

Auth Keys

Owner
Single Signature
Public Keys
STM5JBNRJKnW9hTWAhoDXppLA1FTnJMV7agze5ye8gd1Zb4ZGqcFK1/1
Active
Single Signature
Public Keys
STM5YAtprKXbKameJcDhnwxELYvTH953VQRCeU6VuvnvEWeCXnor71/1
Posting
Single Signature
Public Keys
STM6YK6wa24fPaeyVcvS5a2EKarRR7b2RwwHvgo6cDwGPd24sxBD51/1
Memo
STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i
{
  "owner": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5JBNRJKnW9hTWAhoDXppLA1FTnJMV7agze5ye8gd1Zb4ZGqcFK",
        1
      ]
    ]
  },
  "active": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM5YAtprKXbKameJcDhnwxELYvTH953VQRCeU6VuvnvEWeCXnor7",
        1
      ]
    ]
  },
  "posting": {
    "weight_threshold": 1,
    "account_auths": [],
    "key_auths": [
      [
        "STM6YK6wa24fPaeyVcvS5a2EKarRR7b2RwwHvgo6cDwGPd24sxBD5",
        1
      ]
    ]
  },
  "memo": "STM7CM58Wy6igVgUf2o9F9G5FCdjCvLNFcjqc7TaXtQJ4Yqs9x21i"
}

Witness Votes

0 / 30
No active witness votes.
[]