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 DelegationsDeleg
+4.881SP
Detailed Balance
| STEEM | ||
| balance | 0.001STEEM | STEEM |
| market_balance | 0.000STEEM | STEEM |
| savings_balance | 0.000STEEM | STEEM |
| reward_steem_balance | 0.072STEEM | STEEM |
| STEEM POWER | ||
| Own SP | 0.126SP | SP |
| Delegated Out | 0.000SP | SP |
| Delegation In | 4.881SP | SP |
| Effective Power | 5.007SP | SP |
| Reward SP (pending) | 0.132SP | SP |
| SBD | ||
| sbd_balance | 0.000SBD | SBD |
| sbd_conversions | 0.000SBD | SBD |
| sbd_market_balance | 0.000SBD | SBD |
| savings_sbd_balance | 0.000SBD | SBD |
| reward_sbd_balance | 0.000SBD | SBD |
{
"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
| name | typenil |
| id | 794214 |
| rank | 527,359 |
| reputation | 6141895570 |
| created | 2018-03-02T19:34:15 |
| recovery_account | steem |
| proxy | None |
| post_count | 5 |
| comment_count | 0 |
| lifetime_vote_count | 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 |
| proxied_vsf_votes | 0, 0, 0, 0 |
| can_vote | 1 |
| voting_power | 0 |
| delayed_votes | 0 |
| balance | 0.001 STEEM |
| savings_balance | 0.000 STEEM |
| sbd_balance | 0.000 SBD |
| savings_sbd_balance | 0.000 SBD |
| vesting_shares | 204.284588 VESTS |
| delegated_vesting_shares | 0.000000 VESTS |
| received_vesting_shares | 7939.375218 VESTS |
| reward_vesting_balance | 263.453058 VESTS |
| vesting_balance | 0.000 STEEM |
| vesting_withdraw_rate | 0.000000 VESTS |
| next_vesting_withdrawal | 1969-12-31T23:59:59 |
| withdrawn | 0 |
| to_withdraw | 0 |
| withdraw_routes | 0 |
| savings_withdraw_requests | 0 |
| last_account_recovery | 1970-01-01T00:00:00 |
| reset_account | null |
| last_owner_update | 1970-01-01T00:00:00 |
| last_account_update | 2019-12-12T16:16:00 |
| mined | No |
| sbd_seconds | 0 |
| sbd_last_interest_payment | 1970-01-01T00:00:00 |
| savings_sbd_last_interest_payment | 1970-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
| Incoming | Outgoing |
|---|---|
Empty | Empty |
{
"incoming": [],
"outgoing": []
}From Date
To Date
2026/05/18 07:44:45
2026/05/18 07:44:45
| delegator | steem |
| delegatee | typenil |
| vesting shares | 7939.375218 VESTS |
| Transaction Info | Block #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"
}
]
}2026/05/13 10:12:54
2026/05/13 10:12:54
| delegator | steem |
| delegatee | typenil |
| vesting shares | 5227.164813 VESTS |
| Transaction Info | Block #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"
}
]
}2026/04/26 06:54:42
2026/04/26 06:54:42
| delegator | steem |
| delegatee | typenil |
| vesting shares | 7951.890974 VESTS |
| Transaction Info | Block #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"
}
]
}2026/01/24 03:55:18
2026/01/24 03:55:18
| delegator | steem |
| delegatee | typenil |
| vesting shares | 5268.711632 VESTS |
| Transaction Info | Block #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"
}
]
}2024/12/17 23:03:51
2024/12/17 23:03:51
| delegator | steem |
| delegatee | typenil |
| vesting shares | 5432.930829 VESTS |
| Transaction Info | Block #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"
}
]
}2023/11/14 14:42:15
2023/11/14 14:42:15
| delegator | steem |
| delegatee | typenil |
| vesting shares | 5602.064361 VESTS |
| Transaction Info | Block #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"
}
]
}2023/09/22 12:05:48
2023/09/22 12:05:48
| delegator | steem |
| delegatee | typenil |
| vesting shares | 8538.973147 VESTS |
| Transaction Info | Block #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"
}
]
}2022/11/03 19:22:18
2022/11/03 19:22:18
| delegator | steem |
| delegatee | typenil |
| vesting shares | 8761.024585 VESTS |
| Transaction Info | Block #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"
}
]
}2022/01/18 00:25:42
2022/01/18 00:25:42
| delegator | steem |
| delegatee | typenil |
| vesting shares | 8981.132186 VESTS |
| Transaction Info | Block #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"
}
]
}2021/06/14 07:33:03
2021/06/14 07:33:03
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9165.326474 VESTS |
| Transaction Info | Block #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"
}
]
}2020/12/11 17:43:57
2020/12/11 17:43:57
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9352.748448 VESTS |
| Transaction Info | Block #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"
}
]
}2020/12/06 11:19:15
2020/12/06 11:19:15
| delegator | steem |
| delegatee | typenil |
| vesting shares | 1912.543513 VESTS |
| Transaction Info | Block #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"
}
]
}2020/12/05 21:21:51
2020/12/05 21:21:51
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9358.956302 VESTS |
| Transaction Info | Block #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"
}
]
}2020/11/03 05:18:45
2020/11/03 05:18:45
| delegator | steem |
| delegatee | typenil |
| vesting shares | 1920.017158 VESTS |
| Transaction Info | Block #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"
}
]
}2020/05/09 12:23:36
2020/05/09 12:23:36
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9561.761661 VESTS |
| Transaction Info | Block #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"
}
]
}2020/05/08 16:59:24
2020/05/08 16:59:24
| delegator | steem |
| delegatee | typenil |
| vesting shares | 1953.311140 VESTS |
| Transaction Info | Block #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"
}
]
}2020/03/12 17:05:21
2020/03/12 17:05:21
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9591.631502 VESTS |
| Transaction Info | Block #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
2020/03/05 12:16:39
| 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! <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 Info | Block #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-tables2019/12/19 16:15:03
typenilreceived 0.072 STEEM, 0.087 SP author reward for @typenil / hijacking-default-django-through-tables
2019/12/19 16:15:03
| author | typenil |
| permlink | hijacking-default-django-through-tables |
| sbd payout | 0.000 SBD |
| steem payout | 0.072 STEEM |
| vesting payout | 141.757006 VESTS |
| Transaction Info | Block #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"
}
]
}kniferupvoted (100.00%) @typenil / hijacking-default-django-through-tables2019/12/18 05:48:51
kniferupvoted (100.00%) @typenil / hijacking-default-django-through-tables
2019/12/18 05:48:51
| voter | knifer |
| author | typenil |
| permlink | hijacking-default-django-through-tables |
| weight | 10000 (100.00%) |
| Transaction Info | Block #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
}
]
}emrebeylerupvoted (100.00%) @typenil / hijacking-default-django-through-tables2019/12/18 05:48:45
emrebeylerupvoted (100.00%) @typenil / hijacking-default-django-through-tables
2019/12/18 05:48:45
| voter | emrebeyler |
| author | typenil |
| permlink | hijacking-default-django-through-tables |
| weight | 10000 (100.00%) |
| Transaction Info | Block #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
}
]
}2019/12/12 17:42:06
2019/12/12 17:42:06
| delegator | steem |
| delegatee | typenil |
| vesting shares | 29441.832530 VESTS |
| Transaction Info | Block #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"
}
]
}abojasim880upvoted (100.00%) @typenil / hijacking-default-django-through-tables2019/12/12 16:22:39
abojasim880upvoted (100.00%) @typenil / hijacking-default-django-through-tables
2019/12/12 16:22:39
| voter | abojasim880 |
| author | typenil |
| permlink | hijacking-default-django-through-tables |
| weight | 10000 (100.00%) |
| Transaction Info | Block #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 properties2019/12/12 16:16:00
typenilupdated their account properties
2019/12/12 16:16:00
| 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"}} |
| Transaction Info | Block #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\"}}"
}
]
}typenilpublished a new post: hijacking-default-django-through-tables2019/12/12 16:15:03
typenilpublished a new post: hijacking-default-django-through-tables
2019/12/12 16:15:03
| 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. 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 Info | Block #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\"}"
}
]
}2019/07/20 21:01:21
2019/07/20 21:01:21
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9729.930188 VESTS |
| Transaction Info | Block #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 properties2019/06/21 19:33:36
typenilupdated their account properties
2019/06/21 19:33:36
| 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"}} |
| Transaction Info | Block #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\"}}"
}
]
}2019/04/20 20:29:36
2019/04/20 20:29:36
| delegator | steem |
| delegatee | typenil |
| vesting shares | 29757.819257 VESTS |
| Transaction Info | Block #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"
}
]
}typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier2019/04/20 18:44:57
typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier
2019/04/20 18:44:57
| 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 @@ 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 Info | Block #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/\"]}"
}
]
}typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier2019/04/20 18:44:18
typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier
2019/04/20 18:44:18
| 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 @@ 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 Info | Block #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-go2019/04/20 18:40:51
typenilpublished a new post: github-oauth2-in-go
2019/04/20 18:40:51
| 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. 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).  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).  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 Info | Block #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\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\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\"}"
}
]
}2019/04/07 04:28:27
2019/04/07 04:28:27
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9790.913413 VESTS |
| Transaction Info | Block #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
2019/03/02 22:13:18
| 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! <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 Info | Block #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
2019/02/26 01:45:54
| 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! 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  |
| json metadata | {"app":"partiko"} |
| Transaction Info | Block #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",
"json_metadata": "{\"app\":\"partiko\"}"
}
]
}2019/01/06 00:10:24
2019/01/06 00:10:24
| delegator | steem |
| delegatee | typenil |
| vesting shares | 29937.776900 VESTS |
| Transaction Info | Block #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"
}
]
}fyrstikkenupvoted (1.00%) @typenil / git-hooks-autoformat-before-commit2019/01/05 22:57:33
fyrstikkenupvoted (1.00%) @typenil / git-hooks-autoformat-before-commit
2019/01/05 22:57:33
| voter | fyrstikken |
| author | typenil |
| permlink | git-hooks-autoformat-before-commit |
| weight | 100 (1.00%) |
| Transaction Info | Block #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
}
]
}2019/01/05 22:43:27
2019/01/05 22:43:27
| 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. |
| Transaction Info | Block #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."
}
]
}typenilpublished a new post: git-hooks-autoformat-before-commit2019/01/05 22:40:15
typenilpublished a new post: git-hooks-autoformat-before-commit
2019/01/05 22:40:15
| 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. 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 Info | Block #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\"}"
}
]
}2018/10/09 23:11:12
2018/10/09 23:11:12
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9891.684275 VESTS |
| Transaction Info | Block #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"
}
]
}typenilreceived 0.075 SP author reward for @typenil / how-to-automate-ghost-medium-cross-posting-via-zapier2018/07/17 21:30:36
typenilreceived 0.075 SP author reward for @typenil / how-to-automate-ghost-medium-cross-posting-via-zapier
2018/07/17 21:30:36
| 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 |
| Transaction Info | Block #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"
}
]
}hr1upvoted (0.02%) @typenil / how-to-automate-ghost-medium-cross-posting-via-zapier2018/07/10 22:01:00
hr1upvoted (0.02%) @typenil / how-to-automate-ghost-medium-cross-posting-via-zapier
2018/07/10 22:01:00
| voter | hr1 |
| author | typenil |
| permlink | how-to-automate-ghost-medium-cross-posting-via-zapier |
| weight | 2 (0.02%) |
| Transaction Info | Block #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
}
]
}typenilupdated options for how-to-automate-ghost-medium-cross-posting-via-zapier2018/07/10 21:30:36
typenilupdated options for how-to-automate-ghost-medium-cross-posting-via-zapier
2018/07/10 21:30:36
| 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 | [] |
| Transaction Info | Block #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": []
}
]
}typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier2018/07/10 21:30:36
typenilpublished a new post: how-to-automate-ghost-medium-cross-posting-via-zapier
2018/07/10 21:30:36
| 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> <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 Info | Block #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
2018/07/01 20:18:27
| required auths | [] |
| required posting auths | ["typenil"] |
| id | follow |
| json | ["follow",{"follower":"typenil","following":"cmichel","what":["blog"]}] |
| Transaction Info | Block #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 properties2018/07/01 19:47:06
typenilupdated their account properties
2018/07/01 19:47:06
| 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/"}} |
| Transaction Info | Block #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 properties2018/07/01 19:39:39
typenilupdated their account properties
2018/07/01 19:39:39
| 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/"}} |
| Transaction Info | Block #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/\"}}"
}
]
}magpieloverupvoted (100.00%) @typenil / sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories2018/07/01 02:32:30
magpieloverupvoted (100.00%) @typenil / sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
2018/07/01 02:32:30
| voter | magpielover |
| author | typenil |
| permlink | sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories |
| weight | 10000 (100.00%) |
| Transaction Info | Block #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
2018/07/01 02:26:42
| 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 | |
| Transaction Info | Block #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
2018/07/01 02:26:42
| voter | introduce.bot |
| author | typenil |
| permlink | sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories |
| weight | 38 (0.38%) |
| Transaction Info | Block #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
2018/07/01 01:59:24
| voter | alphabot |
| author | typenil |
| permlink | sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories |
| weight | 100 (1.00%) |
| Transaction Info | Block #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
}
]
}typenilpublished a new post: sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories2018/07/01 01:59:09
typenilpublished a new post: sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
2018/07/01 01:59:09
| 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 @@ 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 Info | Block #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
2018/07/01 01:56:45
| 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 | # # 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 Info | Block #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\"}"
}
]
}ax3upvoted (1.00%) @typenil / sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories2018/07/01 01:56:42
ax3upvoted (1.00%) @typenil / sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
2018/07/01 01:56:42
| voter | ax3 |
| author | typenil |
| permlink | sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories |
| weight | 100 (1.00%) |
| Transaction Info | Block #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
}
]
}typenilpublished a new post: sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories2018/07/01 01:56:30
typenilpublished a new post: sqlalchemy-factoryboy-passing-arbitrary-sessions-to-factories
2018/07/01 01:56:30
| 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.  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 Info | Block #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\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\"}"
}
]
}2018/06/28 21:02:06
2018/06/28 21:02:06
| delegator | steem |
| delegatee | typenil |
| vesting shares | 30249.630303 VESTS |
| Transaction Info | Block #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 properties2018/06/28 19:48:00
typenilupdated their account properties
2018/06/28 19:48:00
| 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/"}} |
| Transaction Info | Block #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/\"}}"
}
]
}2018/06/01 22:18:15
2018/06/01 22:18:15
| delegator | steem |
| delegatee | typenil |
| vesting shares | 9961.405869 VESTS |
| Transaction Info | Block #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"
}
]
}2018/03/02 21:04:39
2018/03/02 21:04:39
| delegator | steem |
| delegatee | typenil |
| vesting shares | 30438.322073 VESTS |
| Transaction Info | Block #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"
}
]
}2018/03/02 19:34:15
2018/03/02 19:34:15
| 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 | [] |
| Transaction Info | Block #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": []
}
]
}Manabar
Voting Power100.00%
Downvote Power100.00%
Resource Credits100.00%
Reputation Progress9.47%
{
"voting_manabar": {
"current_mana": "8143659806",
"last_update_time": 1779090285
},
"downvote_manabar": {
"current_mana": 2035914951,
"last_update_time": 1779090285
},
"rc_account": {
"account": "typenil",
"rc_manabar": {
"current_mana": "10164408779",
"last_update_time": 1779090285
},
"max_rc_creation_adjustment": {
"amount": "2020748973",
"precision": 6,
"nai": "@@000000037"
},
"max_rc": "10164408779"
}
}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.
[]