jacebrowning__memegen METASEP

app/tests/images/ptj/_/_/_/_/_/_/Has_a_watermark./Doesn't_have_a_watermark!.c08b86226b17c14c34213d1972cce0c81e93b88e.png METASEP

app/tests/images/ptj/Exit/Exit/the/the/monorepo/monorepo/Exit_the_monorepo./Stop_testing!.a09156ee7a3d06dbc118da9c58fd0a5e04448b81.png METASEP

app/tests/images/mouth/Sales_Team_presenting_solution_that_won't_work/Excited_Customer/Me.f4eedb6e3e6f25bb652a85268b5a1ad29026d8be.png METASEP

app/tests/images/ds/one/two/three.374d2c1d195e5120b3e977309705c2d404b5de80.png METASEP

app/tests/images/ds/Push_this_button./Push_that_button./can't_decide_which_is_worse.f0d1da8fd7ae04fb957e3ebf52d382bf8b2f5b61.png METASEP

app/tests/images/toohigh/_/the_number_of_sample_memes_is_too_damn_high!.4446dff98bfc3fcf392276e7b823c40595966837.png METASEP

app/tests/images/spongebob/You:_Stop_talking_like_that/Me:_Stop_talking_like_that.062516ea988689e3a148a90ee09966b73537da92.png METASEP

app/tests/images/sparta/_/this_is_a_wide_image!.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/ski/if_you_try_to_put_a_bunch_more_text_than_can_possibly_fit_on_a_meme/you're_gonna_have_a_bad_time.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/pigeon/_/multiple_words_here.f78370156b5bb0c5f6b2abbf43e027d2d98946ba.png METASEP

app/tests/images/oprah/you_get_animated_text/and_you_get_animated_text.75e650dccf742f573923742b89b1041baa13e463.gif METASEP

app/tests/images/iw/tests_code/in_production_(debug).a93ae473d60f658f0225ebcf1d8e9c27d9b4ef4a.png METASEP

app/tests/images/iw/tests_code/in_production_(debug).a93ae473d60f658f0225ebcf1d8e9c27d9b4ef4a.gif METASEP

app/tests/images/iw/tests_code/in_production.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/iw/tests_code/in_production.f758951228f4c53c2b0348a12f4db56ca39e4c85.jpg METASEP

app/tests/images/icanhas/width=600/height=400.48e0a379b111606509fbe660098fccc30c64d467.png METASEP

app/tests/images/icanhas/width=50/height=50.60757e63d5bbc597a36eb78eff0057afc1c8e2a4.png METASEP

app/tests/images/icanhas/width=400/height=600.3fd31d00518b127414bba45640697a105709a709.png METASEP

app/tests/images/icanhas/width=2000/height=2000.05e7b82b12d295b2657d702c5acbc322c115d017.png METASEP

app/tests/images/icanhas/small_image/with_watermark_(disabled).24c9402703c9d0dbddeab69f4da2ca8f53468bd4.png METASEP

app/tests/images/icanhas/paddded_image/with_watermark.6fc12f4c376e9dd8d521b028bfda4da735e25d68.png METASEP

app/tests/images/icanhas/nominal_image/with_watermark.8a5415247019698fe431c3d4f76e9b750b90f220.png METASEP

app/tests/images/icanhas/custom/font.8bf23795c6bb0af6522f1aee2ddc22b79dd7f582.png METASEP

app/tests/images/icanhas/Special~q_100~p_~hthese--memes/template__rating:_9~s10.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/fry/short_line/longer_line_of_text_than_the_short_one.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/fry/longer_line_of_text_than_the_short_one/short_line.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/fry/a/b.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/fine/_/thiiiiiiiiiiiiiiiiiiiiis_will_probably_be_fine_right_now.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/fine/102_°F/this_is_fine.05b7ccdf1e024660c57a3be24c85d5d482433234.png METASEP

app/tests/images/_custom-b33b21e00db2e2e7ba2499373f0cca9e0cdb203f/_/My_Custom_Template.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/_custom-0ddf0e0b5de4648b882d2763a4aea1deca14f3d1/_/This_should_not_be_rotated!.f758951228f4c53c2b0348a12f4db56ca39e4c85.png METASEP

app/tests/images/same/_.34c874bba7e220a230d54569ece0c6746a4c12c9.png METASEP

app/tests/images/icanhas/width=500.d0ed33eb790a99cf48494b1f98d49214c7e90d4f.png METASEP

app/tests/images/icanhas/width=250.f655cdc2dc83cf246ea2aa3ad7b4e27ce4cc60bb.png METASEP

app/tests/images/icanhas/height=500.b41a442b757321eb8c5b82de50d22de64cf328ba.png METASEP

app/tests/images/icanhas/height=250.8cf88d2c2314a9ee5674e49fedfc7da96e796495.png METASEP

app/tests/images/ds/this_button_seems_to_be_ok_to_push.f0d1da8fd7ae04fb957e3ebf52d382bf8b2f5b61.png METASEP

app/tests/images/cmm/Many~nextra~nlines~nof~ntext.6be3db396efd9da69d2959cddd05499285c56029.png METASEP

app/tests/images/_error/UNKNOWN_TEMPLATE.acfcf5969a128c5c488304c87857e91792fef22e.png METASEP

templates/zero-wing/default.jpg METASEP

templates/zero-wing/config.yml METASEP
name: All Your Base Are Belong to Us
source: https://knowyourmeme.com/memes/all-your-base-are-belong-to-us
text:
  - style: upper
    color: palegoldenrod
    font: thick
    anchor_x: 0.025
    anchor_y: 0.7
    angle: 0.0
    scale_x: 0.95
    scale_y: 0.265
    start: 0.0
    stop: 1.0
example:
  - all your base are belong to us
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/yuno/default.jpg METASEP

templates/yuno/config.yml METASEP
name: Y U NO Guy
source: http://knowyourmeme.com/memes/y-u-no-guy
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - y u no
  - use this meme!?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/yodawg/default.jpg METASEP

templates/yodawg/config.yml METASEP
name: Xzibit Yo Dawg
source: http://knowyourmeme.com/memes/xzibit-yo-dawg
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - yo dawg
  - i heard you like memes
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/yallgot/default.jpg METASEP

templates/yallgot/config.yml METASEP
name: Y'all Got Any More of Them
source: http://knowyourmeme.com/memes/yall-got-anymore-of
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - y'all got any more of them
  - memes
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/xy/default.jpg METASEP

templates/xy/config.yml METASEP
name: X all the Y
source: http://knowyourmeme.com/memes/x-all-the-y
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - all the things!!!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/worst/default.jpg METASEP

templates/worst/config.yml METASEP
name: The Worst Day Of Your Life So Far
source: https://knowyourmeme.com/memes/the-worst-day-of-your-life-so-far
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.4
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.9
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - This is the worst day of my life.
  - This is the worst day of your life so far.
overlay:
  - center_x: 0.485
    center_y: 0.1
    angle: 0.0
    scale: 0.2

templates/wonka/default.jpg METASEP

templates/wonka/config.yml METASEP
name: Condescending Wonka
source: http://knowyourmeme.com/memes/condescending-wonka-creepy-wonka
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - oh, you just graduated?
  - you must know everything
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/woman-cat/default.jpg METASEP

templates/woman-cat/config.yml METASEP
name: Woman Yelling at a Cat
source: https://knowyourmeme.com/memes/woman-yelling-at-a-cat
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.22
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.5
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.22
    start: 0.0
    stop: 1.0
example:
  - Mom telling me how useless I am
  - 12 year old me playing Minecraft
overlay:
  - center_x: 0.25
    center_y: 0.61
    angle: 0.0
    scale: 0.78
  - center_x: 0.75
    center_y: 0.61
    angle: 0.0
    scale: 0.78

templates/wkh/default.jpg METASEP

templates/wkh/config.yml METASEP
name: Who Killed Hannibal?
source: https://knowyourmeme.com/memes/who-killed-hannibal
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.1
    anchor_y: 0.3
    angle: 0.0
    scale_x: 0.3
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.53
    anchor_y: 0.35
    angle: 0.0
    scale_x: 0.3
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.875
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.125
    start: 0.0
    stop: 1.0
example:
  - Twin Towers
  - George Bush
  - Why would Al-Qaeda do this?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/winter/default.jpg METASEP

templates/winter/config.yml METASEP
name: Winter is coming
source: http://knowyourmeme.com/memes/imminent-ned-brace-yourselves-winter-is-coming
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - prepare yourself
  - winter is coming
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/whatyear/default.jpg METASEP

templates/whatyear/config.yml METASEP
name: What Year Is It?
source: http://knowyourmeme.com/memes/what-year-is-it
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - what year is it?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/waygd/default.jpg METASEP

templates/waygd/config.yml METASEP
name: What Are Ya Gonna Do?
source: https://www.youtube.com/watch?v=76URh0S7YIw
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.2
    stop: 1.0
example:
  - yeah...
  - what are ya gonna do?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/waygd/animated.gif METASEP

templates/vince/default.jpg METASEP

templates/vince/config.yml METASEP
name: Vince McMahon Reaction
source: https://knowyourmeme.com/memes/vince-mcmahon-reaction
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.495
    scale_y: 0.315
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.32
    angle: 0.0
    scale_x: 0.495
    scale_y: 0.357
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.68
    angle: 0.0
    scale_x: 0.495
    scale_y: 0.32
    start: 0.0
    stop: 1.0
example:
  - 2-Day Delivery
  - Overnight Shipping
  - Shopping at the Store
overlay:
  - center_x: 0.25
    center_y: 0.155
    angle: 0.0
    scale: 0.3
  - center_x: 0.25
    center_y: 0.5
    angle: 0.0
    scale: 0.3
  - center_x: 0.25
    center_y: 0.84
    angle: 0.0
    scale: 0.3

templates/ugandanknuck/default.jpg METASEP

templates/ugandanknuck/config.yml METASEP
name: Ugandan Knuckles
source: http://knowyourmeme.com/memes/ugandan-knuckles
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - do u know de wey?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/trump/default.jpg METASEP

templates/trump/config.yml METASEP
name: Donald Trump
source: https://knowyourmeme.com/memes/people/donald-trump
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - this is the best meme in the history of memes
  - maybe ever
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/tried/default.jpg METASEP

templates/tried/config.yml METASEP
name: At Least You Tried
source: http://knowyourmeme.com/memes/you-tried
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - at least
  - you tried
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/touch/default.jpg METASEP

templates/touch/config.yml METASEP
name: Principal Skinner / Am I Out Of Touch?
source: https://knowyourmeme.com/memes/am-i-out-of-touch
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.3
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Am I so out of touch?
  - No, it's the children who are wrong.
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/toohigh/default.jpg METASEP

templates/toohigh/config.yml METASEP
name: The Rent Is Too Damn High
source: http://knowyourmeme.com/memes/the-rent-is-too-damn-high-jimmy-mcmillan
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - the rent is
  - too damn high
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/tenguy/default.jpg METASEP

templates/tenguy/config.yml METASEP
name: 10 Guy
source: http://knowyourmeme.com/memes/10-guy
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - can't read the words on the menu
  - turns down the radio
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/success/default.jpg METASEP

templates/success/config.yml METASEP
name: Success Kid
source: http://knowyourmeme.com/memes/success-kid-i-hate-sandcastles
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - don't know a question on the test
  - answer is in another question
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/stop/default.jpg METASEP

templates/stop/config.yml METASEP
name: Stop It Patrick You're Scaring Him
source: https://knowyourmeme.com/memes/stop-it-patrick-youre-scaring-him
text:
  - style: default
    color: '#b1e0d0'
    font: thin
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.06
    start: 0.0
    stop: 1.0
  - style: default
    color: '#ffb6a6'
    font: thin
    anchor_x: 0.5
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.075
    start: 0.0
    stop: 1.0
  - style: default
    color: '#fff05c'
    font: thin
    anchor_x: 0.18
    anchor_y: 0.335
    angle: 0.0
    scale_x: 0.32
    scale_y: 0.18
    start: 0.0
    stop: 1.0
  - style: default
    color: '#b1e0d0'
    font: thin
    anchor_x: 0.5
    anchor_y: 0.335
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.07
    start: 0.0
    stop: 1.0
  - style: default
    color: '#ffb6a6'
    font: thin
    anchor_x: 0.0
    anchor_y: 0.67
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.075
    start: 0.0
    stop: 1.0
  - style: default
    color: '#fff05c'
    font: thin
    anchor_x: 0.5
    anchor_y: 0.67
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.075
    start: 0.0
    stop: 1.0
example:
  - I'm claustrophobic.
  - What does "claustrophobic" mean?
  - It means he's afraid of Santa Claus.
  - No, it doesn't.
  - HO-HO-HO
  - Stop it, Patrick, you're scaring him!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/stop-it/default.jpg METASEP

templates/stop-it/config.yml METASEP
name: Stop It, Get Some Help
source: https://knowyourmeme.com/memes/stop-it-get-some-help
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - stop it
  - get some help
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/stonks/default.png METASEP

templates/stonks/config.yml METASEP
name: Stonks
source: https://knowyourmeme.com/memes/stonks
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - stonks
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/stew/default.jpg METASEP

templates/stew/config.yml METASEP
name: Baby, You've Got a Stew Going
source: https://www.youtube.com/watch?v=Sr2PlqXw03Y
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - baby, you've got a stew going!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ss/default.jpg METASEP

templates/ss/config.yml METASEP
name: Scumbag Steve
source: http://knowyourmeme.com/memes/scumbag-steve
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - needs a place to crash
  - never leaves
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/spongebob/default.jpg METASEP

templates/spongebob/config.yml METASEP
name: Mocking Spongebob
source: http://knowyourmeme.com/memes/mocking-spongebob
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: mock
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - "BF: I don't even know her like that"
  - "Me: i DoN'T eVeN KnOw HeR lIkE tHaT"
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/spiderman/default.jpg METASEP

templates/spiderman/config.yml METASEP
name: Spider-Man Pointing at Spider-Man
source: https://knowyourmeme.com/memes/spider-man-pointing-at-spider-man
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.05
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.85
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - me pointing at you
  - you pointing at me
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sparta/default.jpg METASEP

templates/sparta/config.yml METASEP
name: This is Sparta!
source: https://www.youtube.com/watch?v=QkWS9PiXekE
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - this. is.
  - sparta!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/soup-nazi/default.jpg METASEP

templates/soup-nazi/config.yml METASEP
name: No Soup for You / Soup Nazi
source: https://knowyourmeme.com/memes/no-soup-for-you-soup-nazi
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - no soup for you!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sohot/default.png METASEP

templates/sohot/config.yml METASEP
name: So Hot Right Now
source: https://www.youtube.com/watch?v=QizsJQNIKhc
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - this meme is
  - so hot right now
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sohappy/default.jpg METASEP

templates/sohappy/config.yml METASEP
name: I Would Be So Happy
source: https://www.youtube.com/watch?v=z_17RHGKNTY
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - if i could use this meme
  - i would be so happy
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/soa/default.jpg METASEP

templates/soa/config.yml METASEP
name: Seal of Approval
source: http://knowyourmeme.com/memes/seal-of-approval
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - first wipe
  - clean toiletpaper
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/snek/default.png METASEP

templates/snek/config.yml METASEP
name: Skeptical Snake
source: https://www.reddit.com/r/JoeRogan/comments/5kga5k/skeptical_snake/
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - when you already checked that one leaf
  - and it starts moving
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ski/default.png METASEP

templates/ski/config.yml METASEP
name: Super Cool Ski Instructor
source: http://knowyourmeme.com/memes/super-cool-ski-instructor
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - you're gonna have a bad time
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sk/default.jpg METASEP

templates/sk/config.yml METASEP
name: Skeptical Third World Kid
source: http://knowyourmeme.com/memes/third-world-success
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you finished your plate
  - because i was starving?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sf/default.jpg METASEP

templates/sf/config.yml METASEP
name: Sealed Fate
source: https://www.reddit.com/r/AdviceAnimals/comments/3fenos/i_like_the_idea_of_the_impending_doom_seal_i_hope/ctnxw3k
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i accidentally used a swear word
  - and i know my mom heard it from the other room
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/scc/default.jpg METASEP

templates/scc/config.yml METASEP
name: Sudden Clarity Clarence
source: http://knowyourmeme.com/memes/sudden-clarity-clarence
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - oh my god
  - those weren't listerine breath strips
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sb/default.jpg METASEP

templates/sb/config.yml METASEP
name: Scumbag Brain
source: http://knowyourmeme.com/memes/scumbag-brain
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - remembers the face
  - but not the name
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sarcasticbear/default.jpg METASEP

templates/sarcasticbear/config.yml METASEP
name: Sarcastic Bear
source: http://knowyourmeme.com/memes/sarcastic-bear
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i'm so sorry
  - i haven't memorized the internet
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/same/default.jpg METASEP

templates/same/config.yml METASEP
name: They're The Same Picture
source: https://knowyourmeme.com/memes/theyre-the-same-picture
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.06
    anchor_y: 0.015
    angle: -12.0
    scale_x: 0.35
    scale_y: 0.3
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.525
    anchor_y: 0.06
    angle: -12.0
    scale_x: 0.45
    scale_y: 0.3
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.45
    anchor_y: 0.775
    angle: 0.0
    scale_x: 0.4
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - π
  - '3'
  - The Bible
overlay:
  - center_x: 0.275
    center_y: 0.19
    angle: -12.0
    scale: 0.3
  - center_x: 0.8
    center_y: 0.25
    angle: -12.0
    scale: 0.32
  - center_x: 0.665
    center_y: 0.65
    angle: 0.0
    scale: 0.3

templates/saltbae/default.jpg METASEP

templates/saltbae/config.yml METASEP
name: Salt Bae
source: http://knowyourmeme.com/memes/saltbae
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - pours salt on it
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sadfrog/default.jpg METASEP

templates/sadfrog/config.yml METASEP
name: Sad Frog / Feels Bad Man
source: http://knowyourmeme.com/memes/feels-bad-man-sad-frog
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - feels bad man
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sad-obama/really.jpg METASEP

templates/sad-obama/mad.jpg METASEP

templates/sad-obama/frown.jpg METASEP

templates/sad-obama/down.jpg METASEP

templates/sad-obama/default.jpg METASEP

templates/sad-obama/config.yml METASEP
name: Sad Barack Obama
source: http://knowyourmeme.com/memes/sad-joe-biden
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sad barack obama
  - doesn't think you'll vote
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sad-obama/bow.jpg METASEP

templates/sad-clinton/sad.jpg METASEP

templates/sad-clinton/nervous.jpg METASEP

templates/sad-clinton/frown.jpg METASEP

templates/sad-clinton/default.jpg METASEP

templates/sad-clinton/config.yml METASEP
name: Sad Bill Clinton
source: https://en.wikipedia.org/wiki/Bill_Clinton
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sad bill clinton
  - doesn't think you'll vote
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sad-clinton/ashamed.jpg METASEP

templates/sad-bush/upset.jpg METASEP

templates/sad-bush/unsure.jpg METASEP

templates/sad-bush/nervous.jpg METASEP

templates/sad-bush/facebook.jpg METASEP

templates/sad-bush/default.jpg METASEP

templates/sad-bush/config.yml METASEP
name: Sad George Bush
source: http://knowyourmeme.com/memes/sad-joe-biden
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sad george bush
  - doesn't think you'll vote
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sad-boehner/what.jpg METASEP

templates/sad-boehner/sad.jpg METASEP

templates/sad-boehner/really.jpg METASEP

templates/sad-boehner/frown.jpg METASEP

templates/sad-boehner/default.jpg METASEP

templates/sad-boehner/config.yml METASEP
name: Sad John Boehner
source: https://en.wikipedia.org/wiki/John_Boehner
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sad john boehner
  - doesn't think you'll vote
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/sad-biden/window.jpg METASEP

templates/sad-biden/scowl.jpg METASEP

templates/sad-biden/down.jpg METASEP

templates/sad-biden/default.jpg METASEP

templates/sad-biden/config.yml METASEP
name: Sad Joe Biden
source: http://knowyourmeme.com/memes/sad-joe-biden
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sad joe biden
  - doesn't think you'll vote
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/rollsafe/default.jpg METASEP

templates/rollsafe/config.yml METASEP
name: Roll Safe
source: http://knowyourmeme.com/memes/roll-safe
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - can't get fired
  - if you don't have a job
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/rollsafe/animated.gif METASEP

templates/right/default.png METASEP

templates/right/config.yml METASEP
name: Anakin and Padme Change the World For the Better
source: https://www.reddit.com/r/MemeTemplatesOfficial/comments/nia6wu/im_going_to_change_the_world_for_the_better_right/
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.09
    anchor_y: 0.01
    angle: 0.0
    scale_x: 0.25
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.65
    anchor_y: 0.01
    angle: 0.0
    scale_x: 0.25
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thin
    anchor_x: 0.0
    anchor_y: 0.399
    angle: 0.0
    scale_x: 0.499
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: deafult
    color: khaki
    font: thin
    anchor_x: 0.502
    anchor_y: 0.399
    angle: 0.0
    scale_x: 0.499
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.502
    anchor_y: 0.9
    angle: 0.0
    scale_x: 0.499
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - Senior Developer
  - Junior Developer
  - Put it in the backlog.
  - So we can fix it later, right?
  - So we can fix it later, right?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/reveal/default.png METASEP

templates/reveal/config.yml METASEP
name: Scooby Doo Reveal
source: https://knowyourmeme.com/memes/lets-see-who-this-really-is
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.04
    anchor_y: 0.3
    angle: 0.0
    scale_x: 0.3
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: upper
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.4
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.04
    anchor_y: 0.8
    angle: 0.0
    scale_x: 0.3
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: upper
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.9
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - Villain
  - Let's see who you really are...
  - Protagonist
  - I knew it!
overlay:
  - center_x: 0.21
    center_y: 0.215
    angle: 0.0
    scale: 0.27
  - center_x: 0.23
    center_y: 0.69
    angle: 0.0
    scale: 0.27

templates/remembers/default.jpg METASEP

templates/remembers/config.yml METASEP
name: Pepperidge Farm Remembers
source: http://knowyourmeme.com/memes/pepperidge-farm-remembers
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - remember this meme?
  - pepperidge farm remembers
overlay:
  - center_x: 0.41
    center_y: 0.6
    angle: 0.0
    scale: 0.25

templates/regret/default.jpg METASEP

templates/regret/config.yml METASEP
name: I Immediately Regret This Decision!
source: https://www.youtube.com/watch?v=gd9ltJGykYc
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - i immediately regret this decision
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/red/default.jpg METASEP

templates/red/config.yml METASEP
name: Oh, Is That What We're Going to Do Today?
source: https://www.imdb.com/title/tt0720046/
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - oh, is that what we're going to do today?
  - we're going to fight?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/puffin/default.jpg METASEP

templates/puffin/config.yml METASEP
name: Unpopular opinion puffin
source: http://knowyourmeme.com/memes/unpopular-opinion-puffin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - I don't find Mitch Hedberg
  - all the funny
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ptj/default.jpg METASEP

templates/ptj/config.yml METASEP
name: Phoebe Teaching Joey
source: https://knowyourmeme.com/photos/1846826-phoebe-teaching-joey
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.175
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.175
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: none
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.425
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: none
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.425
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: none
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.675
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: none
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.675
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.925
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.925
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.05
    start: 0.0
    stop: 1.0
example:
  - Cast it
  - Cast it
  - into
  - into
  - the fire
  - the fire
  - Cast it into the fire.
  - Keep the Ring of Power!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/pigeon/default.jpg METASEP

templates/pigeon/config.yml METASEP
name: Is This a Pigeon?
source: https://knowyourmeme.com/memes/is-this-a-pigeon
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.15
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.35
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.71
    anchor_y: 0.3
    angle: 0.0
    scale_x: 0.175
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Engineer
  - PowerPoint
  - Is this Photoshop?
overlay:
  - center_x: 0.8
    center_y: 0.2
    angle: 0.0
    scale: 0.4

templates/philosoraptor/default.jpg METASEP

templates/philosoraptor/config.yml METASEP
name: Philosoraptor
source: http://knowyourmeme.com/memes/philosoraptor
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - if the earth is the third planet from the sun
  - then isn't every country a third world country?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/persian/default.jpg METASEP

templates/persian/config.yml METASEP
name: Persian Cat Room Guardian
source: https://knowyourmeme.com/memes/persian-cat-room-guardian
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - when you wake up from a nap
  - and your mom starts yelling at you
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/perfection/default.jpg METASEP

templates/perfection/config.yml METASEP
name: I Prefer the Real Raven / Perfection
source: https://knowyourmeme.com/memes/perfection
text:
  - style: upper
    color: black
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.333
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.5
    anchor_y: 0.225
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: upper
    color: black
    font: thick
    anchor_x: 0.0
    anchor_y: 0.333
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.333
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.5
    anchor_y: 0.56
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: upper
    color: black
    font: thick
    anchor_x: 0.0
    anchor_y: 0.666
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.333
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.5
    anchor_y: 0.895
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - Sebastian Shaw
  - I prefer the real Darth Vader.
  - Hayden Christensen
  - I said, the real Darth Vader.
  - Jake Lloyd
  - Perfection.
overlay:
  - center_x: 0.25
    center_y: 0.166
    angle: 0.0
    scale: 0.333
  - center_x: 0.25
    center_y: 0.5
    angle: 0.0
    scale: 0.333
  - center_x: 0.25
    center_y: 0.833
    angle: 0.0
    scale: 0.333

templates/patrick/default.jpg METASEP

templates/patrick/config.yml METASEP
name: Push it somewhere else Patrick
source: http://knowyourmeme.com/memes/push-it-somewhere-else-patrick
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.15
    start: 0.15
    stop: 0.55
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.85
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.15
    start: 0.55
    stop: 1.0
example:
  - why dont we take all the memes
  - and put them on memegen
overlay:
  - center_x: 0.1
    center_y: 0.45
    angle: 0.0
    scale: 0.2
  - center_x: 0.775
    center_y: 0.85
    angle: 0.0
    scale: 0.25

templates/patrick/animated.gif METASEP

templates/panik-kalm-panik/default.png METASEP

templates/panik-kalm-panik/config.yml METASEP
name: Panik Kalm Panik
source: https://knowyourmeme.com/memes/panik-kalm
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.01
    anchor_y: 0.01
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.32
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.01
    anchor_y: 0.35
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.32
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.01
    anchor_y: 0.69
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.305
    start: 0.0
    stop: 1.0
example:
  - You hear a sound downstairs
  - It's just a cat
  - You don't have a cat
overlay:
  - center_x: 0.75
    center_y: 0.8
    angle: 0.0
    scale: 0.25

templates/oprah/default.jpg METASEP

templates/oprah/config.yml METASEP
name: Oprah You Get a Car
source: http://knowyourmeme.com/memes/oprah-you-get-a-car
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.1
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.55
    stop: 1.0
example:
  - you get a meme
  - and you get a meme
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/oprah/animated.gif METASEP

templates/older/default.jpg METASEP

templates/older/config.yml METASEP
name: An Older Code Sir, But It Checks Out
source: http://knowyourmeme.com/memes/its-an-older-meme-but-it-checks-out
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - it's an older meme sir
  - but it checks out
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/officespace/default.jpg METASEP

templates/officespace/config.yml METASEP
name: That Would Be Great
source: http://knowyourmeme.com/memes/that-would-be-great
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - yeah...
  - that'd be great
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/oag/default.jpg METASEP

templates/oag/config.yml METASEP
name: Overly Attached Girlfriend
source: http://knowyourmeme.com/memes/overly-attached-girlfriend
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i know you received my email
  - because i checked your inbox
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ntot/default.png METASEP

templates/ntot/config.yml METASEP
name: No Take, Only Throw
source: https://knowyourmeme.com/memes/no-take-only-throw
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.055
    anchor_y: 0.08
    angle: 12.0
    scale_x: 0.21
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.46
    anchor_y: 0.15
    angle: 0.0
    scale_x: 0.18
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: upper
    color: black
    font: thick
    anchor_x: 0.7
    anchor_y: 0.135
    angle: 0.0
    scale_x: 0.25
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - Pls throw??
  - NO TAKE!!
  - ONLY THROW
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/noidea/default.jpg METASEP

templates/noidea/config.yml METASEP
name: I Have No Idea What I'm Doing
source: http://knowyourmeme.com/memes/i-have-no-idea-what-i-m-doing
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i have no idea
  - what i'm doing
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/nice/default.jpg METASEP

templates/nice/config.yml METASEP
name: So I Got That Goin' For Me, Which is Nice
source: http://knowyourmeme.com/memes/so-i-got-that-goin-for-me-which-is-nice
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - so i got that goin' for me, which is nice
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/mw/default.jpg METASEP

templates/mw/config.yml METASEP
name: I Guarantee It
source: http://knowyourmeme.com/memes/george-zimmer-i-guarantee-it
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you're gonna like the way you look
  - i guarantee it
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/mouth/default.png METASEP

templates/mouth/config.yml METASEP
name: Woman Holding Dog's Mouth
source: https://knowyourmeme.com/memes/women-holding-dogs-mouth
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.15
    angle: 0.0
    scale_x: 0.4
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.6
    anchor_y: 0.2
    angle: 0.0
    scale_x: 0.4
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.1
    anchor_y: 0.6
    angle: 0.0
    scale_x: 0.4
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Sales Team presenting the solution in PowerPoint
  - Excited Customer
  - Engineering Team knowing the solution is not technically possible
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/morpheus/default.jpg METASEP

templates/morpheus/config.yml METASEP
name: Matrix Morpheus
source: http://knowyourmeme.com/memes/matrix-morpheus
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - what if I told you that
  - reality was an illusion?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/mordor/default.jpg METASEP

templates/mordor/config.yml METASEP
name: One Does Not Simply Walk into Mordor
source: http://knowyourmeme.com/memes/one-does-not-simply-walk-into-mordor
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - one does not simply
  - walk into mordor
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/money/default.jpg METASEP

templates/money/config.yml METASEP
name: Shut Up and Take My Money!
source: http://knowyourmeme.com/memes/shut-up-and-take-my-money
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - shut up and
  - take my money!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/money/animated.gif METASEP

templates/mmm/default.jpg METASEP

templates/mmm/config.yml METASEP
name: Minor Mistake Marvin
source: http://knowyourmeme.com/memes/minor-mistake-marvin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Puts ice cream back
  - Into the refrigerator
overlay:
  - center_x: 0.56
    center_y: 0.32
    angle: 0.0
    scale: 0.175

templates/mini-keanu/default.jpg METASEP

templates/mini-keanu/config.yml METASEP
name: Mini Keanu Reeves
source: https://knowyourmeme.com/memes/mini-keanu-reeves
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - me waiting for mom to stop talking on the phone
  - so I can tell her my Pokemon evolved
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/millers/default.png METASEP

templates/millers/config.yml METASEP
name: You Guys Are Getting Paid?
source: https://knowyourmeme.com/photos/1712302-you-guys-are-getting-paid
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.35
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.35
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.85
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.85
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - ''
  - You are making 500 thousand dollars and you were only gonna pay me 30?
  - You're getting 30 grand? I'm getting a thousand!
  - You guys are getting paid?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/michael-scott/default.jpg METASEP

templates/michael-scott/config.yml METASEP
name: Michael Scott No God No
source: https://www.youtube.com/watch?v=31g0YE61PLQ
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - no, god! no god please no!
  - noooooooooooooooo!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/mb/flood.png METASEP

templates/mb/default.jpg METASEP

templates/mb/config.yml METASEP
name: Member Berries
source: http://knowyourmeme.com/memes/member-berries
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - "'member"
  - star wars?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/mb/box.jpg METASEP

templates/mb/animated.gif METASEP

templates/lrv/default.jpg METASEP

templates/lrv/config.yml METASEP
name: Laundry Room Viking
source: http://knowyourmeme.com/memes/laundry-room-viking
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - do the laundry they said
  - it will be fun they said
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ll/default.jpg METASEP

templates/ll/config.yml METASEP
name: Laughing Lizard
source: http://knowyourmeme.com/memes/laughing-lizard-hhhehehe
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - hhhehehe
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/live/default.jpg METASEP

templates/live/config.yml METASEP
name: Do It Live!
source: http://knowyourmeme.com/memes/bill-oreilly-rant
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - do it live!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/live/animated.gif METASEP

templates/leo/default.jpg METASEP

templates/leo/config.yml METASEP
name: Leo Strutting
source: https://knowyourmeme.com/memes/strutting-leo
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - I have no idea
  - what I am doing
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/kramer/seinfeld.jpg METASEP

templates/kramer/default.png METASEP

templates/kramer/config.yml METASEP
name: Kramer, What's Going On In There?
source: https://knowyourmeme.com/memes/kramer-whats-going-on-in-there
text:
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.3
    anchor_y: 0.12
    angle: 0.0
    scale_x: 0.7
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 0.6
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Kramer, what's going on in there?
  - It's a Chicken Roaster sign, right across from my window.
overlay:
  - center_x: 0.5
    center_y: 0.505
    angle: 0.0
    scale: 0.4

templates/kombucha/default.png METASEP

templates/kombucha/config.yml METASEP
name: Kombucha Girl
source: https://www.buzzfeed.com/daniellaemanuel/woman-trying-kombucha-meme
text:
  - style: default
    color: black
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.3
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thick
    anchor_x: 0.5125
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.4875
    scale_y: 0.3
    start: 0.0
    stop: 1.0
example:
  - Trying a thing.
  - Trying a thing again.
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/kk/luke.jpg METASEP

templates/kk/default.jpg METASEP

templates/kk/config.yml METASEP
name: Karate Kyle
source: https://knowyourmeme.com/memes/karate-kyle
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.85
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - they broke my pencils
  - i broke their neck
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/khaby-lame/default.jpg METASEP

templates/khaby-lame/config.yml METASEP
name: Khaby Lame Shrug / It's That Sample
source: https://knowyourmeme.com/photos/2132291-khaby-lame-shrug-its-that-simple
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.02
    anchor_y: 0.02
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.47
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.02
    anchor_y: 0.51
    angle: 0.0
    scale_x: 0.45
    scale_y: 0.47
    start: 0.0
    stop: 1.0
example:
  - Bad mood?
  - Laugh at memes
overlay:
  - center_x: 0.25
    center_y: 0.255
    angle: 0.0
    scale: 0.48
  - center_x: 0.25
    center_y: 0.745
    angle: 0.0
    scale: 0.48

templates/kermit/default.jpg METASEP

templates/kermit/config.yml METASEP
name: But That's None of My Business
source: http://knowyourmeme.com/memes/but-thats-none-of-my-business
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - but that's none of my business
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/keanu/default.jpg METASEP

templates/keanu/config.yml METASEP
name: Conspiracy Keanu
source: http://knowyourmeme.com/memes/conspiracy-keanu
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - what if the CIA invented dinosaurs
  - to discourage time travel?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/jw/default.png METASEP

templates/jw/config.yml METASEP
name: Probably Not a Good Idea
source: https://www.youtube.com/watch?v=RFinNxS5KN4&t=86
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you just went and made a new dinosaur?
  - probably not a good idea
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/jw/alternate.png METASEP

templates/joker/default.jpg METASEP

templates/joker/config.yml METASEP
name: It's Simple, Kill the Batman
source: https://www.youtube.com/watch?v=f_XHjqABQQA
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - it's simple
  - kill the batman
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/jetpack/default.jpg METASEP

templates/jetpack/config.yml METASEP
name: Nothing To Do Here
source: http://knowyourmeme.com/memes/nothing-to-do-here-jet-pack-guy
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - nothing to do here
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/jd/default.jpg METASEP

templates/jd/config.yml METASEP
name: Joseph Ducreux / Archaic Rap
source: https://knowyourmeme.com/memes/joseph-ducreux-archaic-rap
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - disregard females
  - acquire currency
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/iw/default.png METASEP

templates/iw/config.yml METASEP
name: Insanity Wolf
source: http://knowyourmeme.com/memes/insanity-wolf
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - does testing
  - in production
overlay:
  - center_x: 0.505
    center_y: 0.515
    angle: 0.0
    scale: 0.525

templates/ive/default.png METASEP

templates/ive/config.yml METASEP
name: Jony Ive Redesigns Things
source: https://www.apple.com/pr/bios/jonathan-ive.html
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - we think
  - you'll love it
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/interesting/default.jpg METASEP

templates/interesting/config.yml METASEP
name: The Most Interesting Man in the World
source: http://knowyourmeme.com/memes/the-most-interesting-man-in-the-world
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - I don't often drink beer
  - so you shouldn't value my opinion
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/inigo/default.jpg METASEP

templates/inigo/config.yml METASEP
name: Inigo Montoya
source: http://knowyourmeme.com/memes/you-keep-using-that-word-i-do-not-think-it-means-what-you-think-it-means
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you keep using that word
  - i do not think it means what you think it means
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/imsorry/default.jpg METASEP

templates/imsorry/config.yml METASEP
name: Oh, I'm Sorry, I Thought This Was America
source: http://knowyourmeme.com/memes/oh-im-sorry-i-thought-this-was-america
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - oh, i'm sorry
  - i thought this was america
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/icanhas/default.jpg METASEP

templates/icanhas/config.yml METASEP
name: I Can Has Cheezburger?
source: http://knowyourmeme.com/memes/happy-cat
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i can has
  - this meme?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/home/default.jpg METASEP

templates/home/config.yml METASEP
name: We Have Food at Home
source: https://knowyourmeme.com/memes/we-have-food-at-home
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.001
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.1
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.2
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - 'Me: Can we stop and get food?'
  - 'Mom: We have food at home.'
  - 'Food at home:'
overlay:
  - center_x: 0.5
    center_y: 0.65
    angle: 0.0
    scale: 0.7

templates/hipster/default.jpg METASEP

templates/hipster/config.yml METASEP
name: Hipster Barista
source: http://knowyourmeme.com/memes/hipster-barista
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - puts coffee deal on Groupon
  - rolls eyes when you actually use it
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/headaches/default.png METASEP

templates/headaches/config.yml METASEP
name: Types of Headaches
source: https://knowyourmeme.com/memes/types-of-headaches
text:
  - style: default
    color: black
    font: thick
    anchor_x: 0.5
    anchor_y: 0.64
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.08
    start: 0.0
    stop: 1.0
example:
  - Breaking Production
overlay:
  - center_x: 0.74
    center_y: 0.82
    angle: 0.0
    scale: 0.15

templates/harold/default.jpg METASEP

templates/harold/config.yml METASEP
name: Hide the Pain Harold
source: https://knowyourmeme.com/memes/hide-the-pain-harold
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - when the boss sees
  - your meme tab
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/happening/default.jpg METASEP

templates/happening/config.yml METASEP
name: It's Happening
source: http://knowyourmeme.com/memes/doom-paul-its-happening
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - it's happening
overlay:
  - center_x: 0.425
    center_y: 0.3
    angle: 0.0
    scale: 0.45

templates/happening/animated.gif METASEP

templates/hagrid/default.jpg METASEP

templates/hagrid/config.yml METASEP
name: I Should Not Have Said That
source: https://www.youtube.com/watch?v=498HkRM77gg
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - i should not have said that
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/grumpycat/default.jpg METASEP

templates/grumpycat/config.yml METASEP
name: Grumpy Cat
source: https://imgflip.com/memegenerator/Grumpy-Cat
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i hope that what does not kill you
  - tries again
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/gru/default.jpg METASEP

templates/gru/config.yml METASEP
name: Gru's Plan
source: https://knowyourmeme.com/memes/grus-plan
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.275
    anchor_y: 0.2
    angle: 0.0
    scale_x: 0.2075
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.78
    anchor_y: 0.2
    angle: 0.0
    scale_x: 0.205
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.28
    anchor_y: 0.7
    angle: 0.0
    scale_x: 0.205
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.78
    anchor_y: 0.7
    angle: 0.0
    scale_x: 0.205
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Learn how to make memes.
  - Make a meme.
  - No one likes it.
  - No one likes it.
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/gone/default.jpg METASEP

templates/gone/config.yml METASEP
name: And It's Gone
source: https://knowyourmeme.com/memes/and-its-gone
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - we'll just invest that money
  - "...aaaand it's gone"
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ggg/default.jpg METASEP

templates/ggg/config.yml METASEP
name: Good Guy Greg
source: http://knowyourmeme.com/memes/good-guy-greg
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - sleeps on your couch
  - makes breakfast
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/gears/default.jpg METASEP

templates/gears/config.yml METASEP
name: You Know What Really Grinds My Gears?
source: http://knowyourmeme.com/memes/you-know-what-really-grinds-my-gears
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you know what really grinds my gears?
  - when people post images of cogs that don't mesh
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/gb/default.jpg METASEP

templates/gb/config.yml METASEP
name: Galaxy Brain
source: https://knowyourmeme.com/memes/galaxy-brain
text:
  - style: none
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 0.49
    scale_y: 0.245
    start: 0.0
    stop: 1.0
  - style: none
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.255
    angle: 0.0
    scale_x: 0.49
    scale_y: 0.246
    start: 0.0
    stop: 1.0
  - style: none
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.51
    angle: 0.0
    scale_x: 0.489
    scale_y: 0.224
    start: 0.0
    stop: 1.0
  - style: none
    color: black
    font: thin
    anchor_x: 0.0
    anchor_y: 0.746
    angle: 0.0
    scale_x: 0.49
    scale_y: 0.255
    start: 0.0
    stop: 1.0
example:
  - Who
  - Whom
  - Whom'st
  - Whomst'd
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/gandalf/default.jpg METASEP

templates/gandalf/config.yml METASEP
name: Confused Gandalf
source: http://knowyourmeme.com/memes/confused-gandalf
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - I have no memory of this place
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fwp/default.jpg METASEP

templates/fwp/config.yml METASEP
name: First World Problems
source: http://knowyourmeme.com/memes/first-world-problems
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fry/default.png METASEP

templates/fry/config.yml METASEP
name: Futurama Fry
source: http://knowyourmeme.com/memes/futurama-fry-not-sure-if
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - not sure if trolling
  - or just stupid
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fry/animated.gif METASEP

templates/fmr/default.jpg METASEP

templates/fmr/config.yml METASEP
name: Fuck Me, Right?
source: http://knowyourmeme.com/memes/fuck-me-right
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - fuck me, right?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/firsttry/default.png METASEP

templates/firsttry/config.yml METASEP
name: First Try!
source: https://www.youtube.com/watch?v=44-RsrF_V_w
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - first try!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fine/default.png METASEP

templates/fine/config.yml METASEP
name: This is Fine
source: http://gunshowcomic.com/648
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - this is fine
overlay:
  - center_x: 0.425
    center_y: 0.54
    angle: 0.0
    scale: 0.5

templates/fine/animated.gif METASEP

templates/fetch/default.jpg METASEP

templates/fetch/config.yml METASEP
name: Stop Trying to Make Fetch Happen
source: http://knowyourmeme.com/memes/stop-trying-to-make-fetch-happen
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - stop trying to make fetch happen
  - it's not going to happen
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/feelsgood/default.png METASEP

templates/feelsgood/config.yml METASEP
name: Feels Good
source: https://knowyourmeme.com/memes/feels-good
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - feels good
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fbf/default.jpg METASEP

templates/fbf/config.yml METASEP
name: Foul Bachelor Frog
source: http://knowyourmeme.com/memes/foul-bachelor-frog
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - paper towel
  - the plate that cleans up after itself
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/facepalm/default.jpg METASEP

templates/facepalm/config.yml METASEP
name: Facepalm
source: http://knowyourmeme.com/memes/facepalm
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - I can't even
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/fa/default.jpg METASEP

templates/fa/config.yml METASEP
name: Forever Alone
source: http://knowyourmeme.com/memes/forever-alone
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - forever
  - alone
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ermg/default.jpg METASEP

templates/ermg/config.yml METASEP
name: Ermahgerd
source: http://knowyourmeme.com/memes/ermahgerd
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ermahgerd
  - memes
overlay:
  - center_x: 0.35
    center_y: 0.45
    angle: 0.0
    scale: 0.3

templates/elf/default.jpg METASEP

templates/elf/config.yml METASEP
name: You Sit on a Throne of Lies
source: http://knowyourmeme.com/memes/you-sit-on-a-throne-of-lies
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - you sit on a throne of lies
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/dwight/default.jpg METASEP

templates/dwight/config.yml METASEP
name: Schrute Facts
source: http://knowyourmeme.com/memes/schrute-facts
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - love is all you need?
  - false. you need water and rations.
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/dsm/default.jpg METASEP

templates/dsm/config.yml METASEP
name: Dating Site Murderer
source: http://knowyourmeme.com/memes/dating-site-murderer
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - they will never find your body
  - as attractive as i do
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ds/maga.jpg METASEP

templates/ds/default.jpg METASEP

templates/ds/config.yml METASEP
name: Daily Struggle
source: https://knowyourmeme.com/memes/daily-struggle
text:
  - style: default
    color: black
    font: thin
    anchor_x: 0.085
    anchor_y: 0.085
    angle: 12.5
    scale_x: 0.32
    scale_y: 0.095
    start: 0.0
    stop: 1.0
  - style: default
    color: black
    font: thin
    anchor_x: 0.455
    anchor_y: 0.055
    angle: 10.5
    scale_x: 0.23
    scale_y: 0.09
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.04
    anchor_y: 0.82
    angle: 0.0
    scale_x: 0.92
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - The dress is black and blue.
  - The dress is gold and white.
overlay:
  - center_x: 0.525
    center_y: 0.55
    angle: 0.0
    scale: 0.4

templates/drowning/default.png METASEP

templates/drowning/config.yml METASEP
name: Drowning High Five
source: https://knowyourmeme.com/memes/drowning-high-five
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.03
    anchor_y: 0.33
    angle: 0.0
    scale_x: 0.46
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.51
    anchor_y: 0.14
    angle: 0.0
    scale_x: 0.2
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.03
    anchor_y: 0.82
    angle: 0.0
    scale_x: 0.46
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - Me Asking for Help
  - Online Commenter
  - I'm having that problem too.
overlay:
  - center_x: 0.585
    center_y: 0.14
    angle: 0.0
    scale: 0.2

templates/drake/yes.jpg METASEP

templates/drake/padding.jpg METASEP

templates/drake/no.jpg METASEP

templates/drake/default.png METASEP

templates/drake/config.yml METASEP
name: Drakeposting
source: http://knowyourmeme.com/memes/drakeposting
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - left on unread
  - left on read
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/drake/beat.jpg METASEP

templates/dragon/default.png METASEP

templates/dragon/config.yml METASEP
name: What Color Do You Want Your Dragon
source: https://knowyourmeme.com/photos/1052192-tumblr
text:
  - style: upper
    color: black
    font: comic
    anchor_x: 0.22
    anchor_y: 0.505
    angle: 0.0
    scale_x: 0.25
    scale_y: 0.119
    start: 0.0
    stop: 1.0
example:
  - OK I want a boyfriend
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/doge/roll.jpg METASEP

templates/doge/pet.jpg METASEP

templates/doge/growl.jpg METASEP

templates/doge/full.jpg METASEP

templates/doge/default.jpg METASEP

templates/doge/config.yml METASEP
name: Doge
source: http://knowyourmeme.com/memes/doge
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - such meme
  - very skill
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/doge/bite.jpg METASEP

templates/doge/bark.jpg METASEP

templates/dodgson/default.jpg METASEP

templates/dodgson/config.yml METASEP
name: See? Nobody Cares
source: http://knowyourmeme.com/memes/see-nobody-cares
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.2
    stop: 0.55
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.65
    stop: 1.0
example:
  - we've got dodgson here!
  - see? nobody cares
overlay:
  - center_x: 0.25
    center_y: 0.2
    angle: 0.0
    scale: 0.2

templates/dodgson/animated.gif METASEP

templates/disastergirl/default.jpg METASEP

templates/disastergirl/config.yml METASEP
name: Disaster Girl
source: http://knowyourmeme.com/memes/disaster-girl
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - just as I planned...
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/dg/default.jpg METASEP

templates/dg/config.yml METASEP
name: Distracted Girlfriend
source: https://knowyourmeme.com/memes/distracted-boyfriend
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.02
    anchor_y: 0.6
    angle: 0.0
    scale_x: 0.35
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.45
    anchor_y: 0.75
    angle: 0.0
    scale_x: 0.25
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.75
    anchor_y: 0.65
    angle: 0.0
    scale_x: 0.225
    scale_y: 0.15
    start: 0.0
    stop: 1.0
example:
  - Socialism
  - The Youth
  - Capitalism
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/db/default.jpg METASEP

templates/db/config.yml METASEP
name: Distracted Boyfriend
source: https://knowyourmeme.com/memes/distracted-boyfriend
text:
  - style: default
    color: white
    font: thick
    anchor_x: 0.12
    anchor_y: 0.7
    angle: 0.0
    scale_x: 0.325
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.55
    anchor_y: 0.45
    angle: 0.0
    scale_x: 0.175
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.74
    anchor_y: 0.66
    angle: 0.0
    scale_x: 0.2
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - Socialism
  - The Youth
  - Capitalism
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/cryingfloor/default.jpg METASEP

templates/cryingfloor/config.yml METASEP
name: Crying on Floor
source: http://knowyourmeme.com/photos/1125083-hold-the-door
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - it's okay
  - lets just reschedule drinking
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/crow/default.jpg METASEP

templates/crow/config.yml METASEP
name: Get Better Material
source: https://knowyourmeme.com/memes/get-better-material
text:
  - style: upper
    color: black
    font: comic
    anchor_x: 0.325
    anchor_y: 0.02
    angle: 0.0
    scale_x: 0.15
    scale_y: 0.15
    start: 0.0
    stop: 1.0
  - style: upper
    color: black
    font: comic
    anchor_x: 0.615
    anchor_y: 0.72
    angle: 12.0
    scale_x: 0.3
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Caw Caw Caw
  - Caw Caw Caw Caw Caw Caw
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/crazypills/default.png METASEP

templates/crazypills/config.yml METASEP
name: I Feel Like I'm Taking Crazy Pills
source: http://knowyourmeme.com/memes/i-feel-like-im-taking-crazy-pills
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - i feel like i'm taking crazy pills
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/cmm/default.png METASEP

templates/cmm/config.yml METASEP
name: Change My Mind
source: https://knowyourmeme.com/memes/steven-crowders-change-my-mind-campus-sign
text:
  - style: upper
    color: black
    font: thin
    anchor_x: 0.33
    anchor_y: 0.33
    angle: 23.0
    scale_x: 0.46
    scale_y: 0.25
    start: 0.0
    stop: 1.0
example:
  - pineapples don't belong on pizza
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/chosen/default.jpg METASEP

templates/chosen/config.yml METASEP
name: You Were the Chosen One!
source: http://knowyourmeme.com/memes/you-were-the-chosen-one
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - you were the chosen one!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/cheems/default.jpg METASEP

templates/cheems/config.yml METASEP
name: Cheems
source: https://knowyourmeme.com/memes/cheems
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - it's a good time to sleep
  - nothing will go wrong after this
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/chair/default.png METASEP

templates/chair/config.yml METASEP
name: American Chopper Argument
source: https://knowyourmeme.com/memes/american-chopper-argument
text:
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.2
    angle: 0.0
    scale_x: 0.495
    scale_y: 0.13
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.495
    anchor_y: 0.2
    angle: 0.0
    scale_x: 0.505
    scale_y: 0.13
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.535
    angle: 0.0
    scale_x: 0.495
    scale_y: 0.13
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.495
    anchor_y: 0.525
    angle: 0.0
    scale_x: 0.505
    scale_y: 0.14
    start: 0.0
    stop: 1.0
  - style: upper
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.87
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.13
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.5
    anchor_y: 0.87
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.13
    start: 0.0
    stop: 1.0
example:
  - Let's expand safety nets
  - Socialism never works!
  - Scandinavia is socialist and they're doing great!
  - They're not socialist. They're capitalist with strong welfare policies!
  - Then let's adopt those!
  - No that's socialism!!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ch/default.jpg METASEP

templates/ch/config.yml METASEP
name: Captain Hindsight
source: http://knowyourmeme.com/memes/captain-hindsight
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - if you wanted to avoid the friend zone
  - you should have made your intentions known from the start
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/center/default.jpg METASEP

templates/center/config.yml METASEP
name: What is this, a Center for Ants?!
source: http://knowyourmeme.com/memes/what-is-this-a-center-for-ants
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - what is this
  - a center for ants
overlay:
  - center_x: 0.325
    center_y: 0.425
    angle: 0.0
    scale: 0.5

templates/cbg/default.jpg METASEP

templates/cbg/config.yml METASEP
name: Comic Book Guy
source: http://simpsons.wikia.com/wiki/Jeffrey_Albertson
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - worst thing ever!
overlay:
  - center_x: 0.88
    center_y: 0.5
    angle: 0.0
    scale: 0.3

templates/cb/default.jpg METASEP

templates/cb/config.yml METASEP
name: Confession Bear
source: http://knowyourmeme.com/memes/confession-bear
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i stole
  - the pic-i-nic basket
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/captain/default.jpg METASEP

templates/captain/config.yml METASEP
name: I am the Captain Now
source: https://www.youtube.com/watch?v=dvA-mimf2yg
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - look at me
  - i am the captain now
overlay:
  - center_x: 0.675
    center_y: 0.5
    angle: 0.0
    scale: 0.35

templates/captain-america/default.jpg METASEP

templates/captain-america/config.yml METASEP
name: Captain America Elevator Fight Dad Joke
source: https://knowyourmeme.com/memes/captain-america-elevator-fight-dad-joke
text:
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.0
    anchor_y: 0.23
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.1
    start: 0.0
    stop: 1.0
  - style: default
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.57
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.08
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thick
    anchor_x: 0.5
    anchor_y: 0.56
    angle: 0.0
    scale_x: 0.5
    scale_y: 0.1
    start: 0.0
    stop: 1.0
example:
  - Have you ever eaten a clock?
  - No, why?
  - It's time consuming.
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/buzz/default.jpg METASEP

templates/buzz/config.yml METASEP
name: X, X Everywhere
source: https://imgflip.com/memetemplate/X-Everywhere
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.05
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.5
    stop: 1.0
example:
  - memes
  - memes everywhere
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/buzz/animated.gif METASEP

templates/bs/default.jpg METASEP

templates/bs/config.yml METASEP
name: This is Bull, Shark
source: https://www.reddit.com/r/funny/comments/3grsfs
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - what a surprise...
  - you caught me again
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/box/default.png METASEP

templates/box/config.yml METASEP
name: What's in the box!? / Seven
source: https://screenrant.com/hilarious-se7en-memes/
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - What's in the box!?
overlay:
  - center_x: 0.83
    center_y: 0.5
    angle: 0.0
    scale: 0.6

templates/box/animated.gif METASEP

templates/both/default.jpg METASEP

templates/both/config.yml METASEP
name: Why Not Both?
source: http://knowyourmeme.com/memes/why-not-both-why-dont-we-have-both
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.2
    stop: 1.0
example:
  - hard or soft tacos
  - why not both?
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/both/animated.gif METASEP

templates/boat/default.jpg METASEP

templates/boat/config.yml METASEP
name: I Should Buy a Boat Cat
source: http://knowyourmeme.com/memes/i-should-buy-a-boat-cat
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - i should buy a boat
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/blb/default.jpg METASEP

templates/blb/config.yml METASEP
name: Bad Luck Brian
source: http://knowyourmeme.com/memes/bad-luck-brian
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - falls asleep in class
  - has a wet dream
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/biw/default.jpg METASEP

templates/biw/config.yml METASEP
name: Baby Insanity Wolf
source: http://knowyourmeme.com/memes/baby-insanity-wolf
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - gets iced coffee
  - in the winter
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/bihw/default.jpg METASEP

templates/bihw/config.yml METASEP
name: But It's Honest Work
source: https://knowyourmeme.com/memes/but-its-honest-work
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - it ain't much
  - but it's honest work
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/bender/default.jpg METASEP

templates/bender/config.yml METASEP
name: I'm Going to Build My Own Theme Park
source: http://knowyourmeme.com/memes/im-going-to-build-my-own-theme-park-with-blackjack-and-hookers
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i'm going to build my own theme park
  - with blackjack and hookers
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/because/default.png METASEP

templates/because/config.yml METASEP
name: Men in Black / Will Smith
source: https://www.youtube.com/watch?v=GNOZhAc26I8&t=76s
text:
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.0
    anchor_y: 0.375
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.125
    start: 0.0
    stop: 1.0
  - style: default
    color: khaki
    font: thin
    anchor_x: 0.0
    anchor_y: 0.875
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.125
    start: 0.0
    stop: 1.0
example:
  - Just because you see a Black man driving in a nice car... does not mean it's stolen.
  - "...I stole that one, but not 'cause I'm Black!"
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/bd/default.jpg METASEP

templates/bd/config.yml METASEP
name: Butthurt Dweller
source: http://knowyourmeme.com/memes/butthurt-dweller-gordo-granudo
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - can't workout
  - don't want to get too buff
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/badchoice/default.jpg METASEP

templates/badchoice/config.yml METASEP
name: Milk Was a Bad Choice
source: https://www.youtube.com/watch?v=DeY0yPqibb0
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - milk
  - was a bad choice
overlay:
  - center_x: 0.25
    center_y: 0.6
    angle: 0.0
    scale: 0.25

templates/bad/default.jpg METASEP

templates/bad/config.yml METASEP
name: You Should Feel Bad
source: http://knowyourmeme.com/memes/your-musics-bad-and-you-should-feel-bad
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - your meme is bad
  - and you should feel bad
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/awkward/default.jpg METASEP

templates/awkward/config.yml METASEP
name: Socially Awkward Penguin
source: http://knowyourmeme.com/memes/socially-awkward-penguin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - start telling joke
  - forget punchline
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/awkward-awesome/default.jpg METASEP

templates/awkward-awesome/config.yml METASEP
name: Socially Awkward Awesome Penguin
source: http://knowyourmeme.com/memes/socially-awkward-penguin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - trip guy on the street
  - he was running with a stolen purse
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/awesome/default.jpg METASEP

templates/awesome/config.yml METASEP
name: Socially Awesome Penguin
source: http://knowyourmeme.com/memes/socially-awesome-penguin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - say a word wrong
  - create hilarious inside joke
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/awesome-awkward/default.jpg METASEP

templates/awesome-awkward/config.yml METASEP
name: Socially Awesome Awkward Penguin
source: http://knowyourmeme.com/memes/socially-awkward-penguin
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - first day at new job
  - spill coffee on bossman
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/away/default.jpg METASEP

templates/away/config.yml METASEP
name: Life... Finds a Way
source: https://www.youtube.com/watch?v=dMjQ3hA9mEA
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - life...
  - finds a way
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/atis/default.jpg METASEP

templates/atis/config.yml METASEP
name: And Then I Said
source: https://knowyourmeme.com/memes/and-then-i-said
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - and then i said
  - the exam will only contain what we've covered in lectures
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/apcr/default.jpg METASEP

templates/apcr/config.yml METASEP
name: Almost Politically Correct Redneck
source: https://knowyourmeme.com/memes/almost-politically-correct-redneck
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - I supported my sister's abortion
  - Still would have been cool to be a dad
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ants/default.jpg METASEP

templates/ants/config.yml METASEP
name: Do You Want Ants?
source: http://knowyourmeme.com/memes/do-you-want-ants
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - do you want ants?
  - because that's how you get ants
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ams/default.jpg METASEP

templates/ams/config.yml METASEP
name: Awkward Moment Seal
source: https://knowyourmeme.com/memes/awkward-moment-seal
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - when you're watching a movie
  - and your parents walk in at the sex scene
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/aint-got-time/default.jpg METASEP

templates/aint-got-time/config.yml METASEP
name: Sweet Brown / Ain't Nobody Got Time For That
source: https://knowyourmeme.com/memes/sweet-brown-aint-nobody-got-time-for-that
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - memes?
  - ain't nobody got time fo' that
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/agnes/default.jpg METASEP

templates/agnes/config.yml METASEP
name: Agnes Harkness Winking
source: https://knowyourmeme.com/memes/agnes-harkness-winking
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - i have read and agree to the terms and conditions
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/afraid/default.jpg METASEP

templates/afraid/config.yml METASEP
name: Afraid to Ask Andy
source: http://knowyourmeme.com/memes/afraid-to-ask-andy
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - i don't know what this meme is for
  - and at this point i'm too afraid to ask
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/ackbar/default.jpg METASEP

templates/ackbar/config.yml METASEP
name: It's A Trap!
source: http://knowyourmeme.com/memes/its-a-trap
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - it's a trap!
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/aag/default.jpg METASEP

templates/aag/config.yml METASEP
name: Ancient Aliens Guy
source: http://knowyourmeme.com/memes/ancient-aliens
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - ''
  - aliens
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/_test/config.yml METASEP
name: Test
source:
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.0
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.0
    anchor_y: 0.8
    angle: 0.0
    scale_x: 1.0
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Top Line
  - Bottom Line
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

templates/_error/default.png METASEP

templates/_error/config.yml METASEP
name: Error
source:
text:
  - style: upper
    color: white
    font: thick
    anchor_x: 0.1
    anchor_y: 0.1
    angle: 0.0
    scale_x: 0.8
    scale_y: 0.2
    start: 0.0
    stop: 1.0
  - style: upper
    color: white
    font: thick
    anchor_x: 0.1
    anchor_y: 0.7
    angle: 0.0
    scale_x: 0.8
    scale_y: 0.2
    start: 0.0
    stop: 1.0
example:
  - Top Line
  - Bottom Line
overlay:
  - center_x: 0.5
    center_y: 0.5
    angle: 0.0
    scale: 0.25

docs/theme/main.html METASEP
{% extends "base.html" %}

<!-- Head -->

{% block extrahead %}
<style>
    table,
    th,
    td {
        border: 1px solid #555;
        background: white;
    }

    th,
    tr,
    td {
        border-top: 1px solid #555 !important;
        border-bottom: 1px solid #555 !important;
    }

    code {
        padding: 2px 4px;
        font-size: 90%;
        color: #e83e8c;
        background-color: #f9f2f4;
        border-radius: 4px;
    }

    @media all and (max-width: 992px) {
        .badges {
            display: none;
        }
    }
</style>
{% endblock %}

<!-- Navigation -->

{% block next_prev %} {% endblock %} {% block repo %} {{ super() }}

<li class="nav-item">
    <a href="https://api.memegen.link/docs/" class="nav-link">
        <i class="fa fa-book"></i>
        Swagger
    </a>
</li>

<li class="nav-item">
    <a href="https://twitter.com/memegenlink" class="nav-link">
        <i class="fa fa-twitter-square"></i>
        Twitter
    </a>
</li>

<li class="nav-item">
    <a href="https://www.facebook.com/memegenlink" class="nav-link">
        <i class="fa fa-facebook-square"></i>
        Facebook
    </a>
</li>

<form class="form-inline m-2 my-lg-0">
    <a
        href="https://memecomplete.com/custom/?standard=true"
        class="btn btn-sm btn-secondary"
    >
        Create a Meme
    </a>
</form>

{% endblock %}

<!-- Content -->

{% block content%} {% if page.title == "Examples" %}

<div class="col-12">
    <div class="row justify-content-center">
        <div class="col-12">{% include "content.html" %}</div>
    </div>
</div>

{% else %}

<div class="col-md-3">{% include "toc.html" %}</div>

<div class="col-md-9 pt-2" role="main">{% include "content.html" %}</div>

{% endif %} {% endblock%}

<!-- Scripts -->

{% block scripts %} {{ super() }}

<script
    src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.2.11/iframeResizer.min.js"
    integrity="sha512-HY1lApSG7xxx8mYzs/lxRs+c5AaDThRaa3pvQB6puiswvf2lWqMJVf+8qSGiL4ZXfHQoPIqbd1TlpqfycPo3cQ=="
    crossorigin="anonymous"
></script>

<script>
    iFrameResize({ checkOrigin: false, resizedCallback: onResized })
    function onResized(iframe, height, width, type) {
        $("#loading").hide()
    }
</script>

{% endblock %}

<!-- Footer -->

{% block footer %} {% if page.title != "Examples" %}

<hr />

<p>
    Copyright © {{ build_date_utc.year }}
    <a href="https://jacebrowning.info">{{ config.site_author }}</a>
</p>

{% endif %} {% endblock %}

docs/img/favicon.ico METASEP

app/tests/images/preview.jpg METASEP

app/tests/images/.flag METASEP

fonts/TitilliumWeb-SemiBold.ttf METASEP

fonts/TitilliumWeb-Black.ttf METASEP

fonts/Tahoma-Bold.ttf METASEP

fonts/Segoe UI Bold.ttf METASEP

fonts/OFL.txt METASEP
Copyright 2012 Google Inc. All Rights Reserved.

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

fonts/NotoSans-Bold.ttf METASEP

fonts/MicroFLF-Bold.ttf METASEP

fonts/Kalam-Regular.ttf METASEP

fonts/Impact.ttf METASEP

docs/logo.png METASEP

docs/index.md METASEP
An API to programmatically generate memes based solely on requested URLs.

<span class="badges"><!-- badges -->
[![Build Status](https://img.shields.io/circleci/build/github/jacebrowning/memegen)](https://circleci.com/gh/jacebrowning/memegen)
[![Coverage Status](http://img.shields.io/coveralls/jacebrowning/memegen/main.svg)](https://coveralls.io/r/jacebrowning/memegen)
[![Swagger Validator](https://img.shields.io/swagger/valid/3.0?label=docs&specUrl=https%3A%2F%2Fapi.memegen.link%2Fdocs%2Fswagger.json)](https://api.memegen.link/docs/)
[![License](https://img.shields.io/badge/license-mit-blue)](https://github.com/jacebrowning/memegen/blob/main/LICENSE.txt)
[![GitHub Sponsors](https://img.shields.io/endpoint?url=https://memecomplete.com/api/memes/badge.json)](https://github.com/sponsors/jacebrowning)
</span>

[Buy me a coffee to help keep this site running!](https://www.buymeacoffee.com/jacebrowning)

---

# Images

The API is stateless so URLs contain all the information necessary to generate meme images. For example, <https://api.memegen.link/images/buzz/memes/memes_everywhere.gif> produces:

![Example Image](https://api.memegen.link/images/buzz/memes/memes_everywhere.gif?token=icc12mvkq7hc9ysbbtxr&width=800&frames=50)

## Available Formats

Clients can request `.jpg` instead of `.png` for smaller files or `.gif` if an animated background is available:

| Format | Example                                                                                                  |
| :----- | :------------------------------------------------------------------------------------------------------- |
| PNG    | [/images/ds/small_file/high_quality.png](https://api.memegen.link/images/ds/small_file/high_quality.png) |
| JPEG   | [/images/ds/high_quality/small_file.jpg](https://api.memegen.link/images/ds/high_quality/small_file.jpg) |
| GIF    | [/images/oprah/you_get/animated_text.gif](https://api.memegen.link/oprah/you_get/animated_text.gif)      |

## Custom Dimensions

Images can be scaled to a specific width or height using the `width=<int>` and `height=<int>` query parameters. If both are provided (`width=<int>&height=<int>`), the image will be padded to the exact dimensions.

For example, <https://api.memegen.link/images/both/width_or_height/why_not_both~q.png?height=450&width=800> produces:

![Custom Size](https://api.memegen.link/images/both/width_or_height/why_not_both~q.png?height=450&width=800&token=e3ctlu471cv4k0hx698p)

## Special Characters

In URLs, spaces can be inserted using underscores or dashes:

- underscore (`_`) → space (` `)
- dash (`-`) → space (` `)
- 2 underscores (`__`) → underscore (`_`)
- 2 dashes (`--`) → dash (`-`)
- tilde + N (`~n`) → newline character

Reserved URL characters can be included using escape patterns:

- tilde + Q (`~q`) → question mark (`?`)
- tilde + A (`~a`) → ampersand (`&`)
- tilde + P (`~p`) → percentage (`%`)
- tilde + H (`~h`) → hashtag/pound (`#`)
- tilde + S (`~s`) → slash (`/`)
- tilde + B (`~b`) → backslash (`\`)
- tilde + L (`~l`) → less-than sign (`<`)
- tilde + G (`~g`) → greater-than sign (`>`)
- 2 single quotes (`''`) → double quote (`"`)

For example, <https://api.memegen.link/images/ugandanknuck/~hspecial_characters~q/underscore__-dash--.png> produces:

![Escaped Characters](https://api.memegen.link/images/ugandanknuck/~hspecial_characters~q/underscore___dash--.png?width=800&token=imyyyyk8qex6m2jz8bar)

All of the `POST` endpoints will return image URLs with special characters replaced with these alternatives.

# Templates

The list of predefined meme templates is available here: <https://api.memegen.link/templates/>

## Alternate Styles

Some memes come in multiple forms, which can be selected using the `style=<str>` query parameter.

For example, the <https://api.memegen.link/templates/ds/> template provides these styles:

|                          `/images/ds.png`                          |                           `/images/ds.png?style=maga`                           |
| :----------------------------------------------------------------: | :-----------------------------------------------------------------------------: |
| ![Default Style](https://api.memegen.link/images/ds.png?width=375) | ![Alternate Style](https://api.memegen.link/images/ds.png?width=375&style=maga) |

## Custom Overlays

The `style=<str>` query parameter can also be an image URL to overlay on the default background image.

For example, <https://api.memegen.link/images/pigeon/Engineer/_/Is_this_Photoshop~q.png?style=https://i.imgur.com/W0NXFpQ.png> produces:

![Custom Overlay](https://api.memegen.link/images/pigeon/Engineer/_/Is_this_Photoshop~q.png?style=https://i.imgur.com/W0NXFpQ.png&width=800&token=cy49tv234bu3jzgw587o)

## Custom Backgrounds

You can also use your own image URL as the background.

For example, <https://api.memegen.link/images/custom/_/my_background.png?background=http://www.gstatic.com/webp/gallery/1.png> produces:

![Custom Background](https://api.memegen.link/images/custom/_/my_background.png?background=http://www.gstatic.com/webp/gallery/1.png&width=800&token=ahby9x2nlsbk0gxdmpo5)

# Fonts

The full list of fonts is available here: <https://api.memegen.link/fonts/>

Add the `font=<str>` query parameter to customize the look of your meme:

| Name                                                                   | ID                  | Alias        |
| ---------------------------------------------------------------------- | ------------------- | ------------ |
| [Titillium Web Black](https://fonts.google.com/specimen/Titillium+Web) | `font=titilliumweb` | `font=thick` |
| [Kalam Regular](https://fonts.google.com/specimen/Kalam)               | `font=kalam`        | `font=comic` |
| [Impact](https://www.dafontfree.io/impact-font/)                       | `font=impact`       | -            |
| [Noto Sans Bold](https://fonts.google.com/noto/specimen/Noto+Sans)     | `font=notosans`     | -            |

<br>

Explore the full API here: <https://api.memegen.link/docs/>

docs/examples.md METASEP
<iframe
    src="http://localhost:5000/examples"
    height="0px"
    width="100%"
    frameBorder="0"
    scrolling="no">
</iframe>
<div class="text-center" width="100%" style="padding-top: 200px;">
    <h2 id="loading" class="text-muted">Generating images...</h2>
</div>

docs/clients.md METASEP
# Sample Code

Here are some popular apps and integrations to check out:

| Platforms   | Language   | Website                                        |
| :---------- | :--------- | :--------------------------------------------- |
| Slack       | Python     | <https://github.com/nicolewhite/slack-meme>    |
| Slack       | Go         | <https://github.com/CptSpaceToaster/slackbot>  |
| Slack       | --         | <http://www.memetizer.com>                     |
| Hain        | JavaScript | <https://github.com/Metrakit/hain-plugin-meme> |
| Web         | Clojure    | <https://github.com/jasich/mighty-fine-memes>  |
| Web & Slack | --         | <https://memecomplete.com>                     |
| Discord     | JavaScript | <https://github.com/parshsee/discordbot>       |
| Legobot     | Python     | <https://github.com/drewpearce/legos.memes>    |

Additional clients can be found by searching for [code examples](https://github.com/search?o=desc&q=%22api.memegen.link%22+&ref=searchresults&s=indexed&type=Code&utf8=%E2%9C%93) on GitHub. Or create your own!

# Authentication

Unauthenticated API requests are watermarked and may be rate limited.
Individuals sponsoring this project at [$10/month](https://github.com/sponsors/jacebrowning/sponsorships?sponsor=jacebrowning&tier_id=55476&preview=false) or more are welcome to an API key.
Please <a href="mailto:support@maketested.com?subject=memegen.link">contact support</a> to request one or inquire about pricing for your specific use case.

## HTTP Header

Authenticated requests can be made using a header:

```shell
$ curl "https://api.memegen.link/images/fry/http_header/example.png" \
  --header "X-API-KEY: myapikey42" --output http_header_example.png --location
```

## Query Parameter

If that's not an option, the API key can also be added as a query parameter:

```shell
$ curl "https://api.memegen.link/images/fry/query_parameter/example.png?api_key=myapikey42" \
  --output query_parameter_example.png --location
```

# Image Previews

If your client is going to show typeahead image previews, please use the `/images/preview.jpg` endpoint, which accepts URL-encoded parameters and returns smaller images to minimize bandwidth:

```javascript
var typingTimer

$("#form").keyup(function () {
  clearTimeout(typingTimer)
  typingTimer = setTimeout(updatePreview, 500)
})

function updatePreview() {
  var template = encodeURIComponent($("#template").val())
  var line_1 = encodeURIComponent($("#line_1").val() || " ")
  var line_2 = encodeURIComponent($("#line_2").val() || " ")

  var api = "https://api.memegen.link/images/preview.jpg"
  var url = `${api}?template=${template}&lines[]=${line_1}&lines[]=${line_2}`

  $("#preview").attr("src", url)
}

$(document).ready(updatePreview)
```

For example, <https://api.memegen.link/images/preview.jpg?template=iw&lines[]=live+preview&lines[]=while+typing> produces:

![Live Preview](https://api.memegen.link/images/preview.jpg?template=iw&lines[]=live+preview&lines[]=while+typing)

The `template=<str>` query parameter can be a template ID or URL:

| Mode              | Example                                                                                                                                                                                                                                                          |
| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Template ID       | [/images/preview.jpg<wbr>?lines[]=first+line&lines[]=second+line<wbr>&template=fry](https://api.memegen.link/images/preview.jpg?lines[]=first+line&lines[]=second+line&template=fry)                                                                             |
| Template URL      | [/images/preview.jpg<wbr>?lines[]=first+line&lines[]=second+line<wbr>&template=https://api.memegen.link/images/fry.png](https://api.memegen.link/images/preview.jpg?lines[]=first+line&lines[]=second+line&template=https://api.memegen.link/images/fry.png)     |
| Custom Background | [/images/preview.jpg<wbr>?lines[]=first+line&lines[]=second+line<wbr>&template=http://www.gstatic.com/webp/gallery/1.png](https://api.memegen.link/images/preview.jpg?lines[]=first+line&lines[]=second+line&template=http://www.gstatic.com/webp/gallery/1.png) |

# Custom Watermark

Authenticated requests can also add their own watermark to images using the `watermark=<str>` query parameter.

For example, <https://api.memegen.link/images/puffin/custom_watermark/example.png?api_key=myapikey42&watermark=example.com> produces:

![](https://api.memegen.link/images/puffin/custom_watermark/example.png?api_key=myapikey42&watermark=example.com&width=800)

# Zapier Integration

Incorporate memes into your workflow automation: <https://zapier.com/apps/memegenlink>

![Sample Zaps](https://cdn.zappy.app/3f3e8213de870f268f27f2402aefc794.png)

bin/verchew METASEP
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# The MIT License (MIT)
# Copyright © 2016, Jace Browning
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Source: https://github.com/jacebrowning/verchew
# Documentation: https://verchew.readthedocs.io
# Package: https://pypi.org/project/verchew


from __future__ import unicode_literals

import argparse
import logging
import os
import re
import sys
from collections import OrderedDict
from subprocess import PIPE, STDOUT, Popen


PY2 = sys.version_info[0] == 2

if PY2:
    import ConfigParser as configparser
    from urllib import urlretrieve
else:
    import configparser
    from urllib.request import urlretrieve

__version__ = '3.1'

SCRIPT_URL = (
    "https://raw.githubusercontent.com/jacebrowning/verchew/master/verchew/script.py"
)

CONFIG_FILENAMES = ['verchew.ini', '.verchew.ini', '.verchewrc', '.verchew']

SAMPLE_CONFIG = """
[Python]

cli = python
version = Python 3.5 || Python 3.6

[Legacy Python]

cli = python2
version = Python 2.7

[virtualenv]

cli = virtualenv
version = 15
message = Only required with Python 2.

[Make]

cli = make
version = GNU Make
optional = true

""".strip()

STYLE = {
    "~": "✔",
    "?": "▴",
    "x": "✘",
    "#": "䷉",
}

COLOR = {
    "~": "\033[92m",  # green
    "?": "\033[93m",  # yellow
    "x": "\033[91m",  # red
    "#": "\033[96m",  # cyan
    None: "\033[0m",  # reset
}

QUIET = False

log = logging.getLogger(__name__)


def main():
    global QUIET

    args = parse_args()
    configure_logging(args.verbose)
    if args.quiet:
        QUIET = True

    log.debug("PWD: %s", os.getenv('PWD'))
    log.debug("PATH: %s", os.getenv('PATH'))

    if args.vendor:
        vendor_script(args.vendor)
        sys.exit(0)

    path = find_config(args.root, generate=args.init)
    config = parse_config(path)

    if not check_dependencies(config) and args.exit_code:
        sys.exit(1)


def parse_args():
    parser = argparse.ArgumentParser(description="System dependency version checker.",)

    version = "%(prog)s v" + __version__
    parser.add_argument(
        '--version', action='version', version=version,
    )
    parser.add_argument(
        '-r', '--root', metavar='PATH', help="specify a custom project root directory"
    )
    parser.add_argument(
        '--exit-code',
        action='store_true',
        help="return a non-zero exit code on failure",
    )

    group_logging = parser.add_mutually_exclusive_group()
    group_logging.add_argument(
        '-v', '--verbose', action='count', default=0, help="enable verbose logging"
    )
    group_logging.add_argument(
        '-q', '--quiet', action='store_true', help="suppress all output on success"
    )

    group_commands = parser.add_argument_group('commands')
    group_commands.add_argument(
        '--init', action='store_true', help="generate a sample configuration file"
    )

    group_commands.add_argument(
        '--vendor', metavar='PATH', help="download the program for offline use"
    )

    args = parser.parse_args()

    return args


def configure_logging(count=0):
    if count == 0:
        level = logging.WARNING
    elif count == 1:
        level = logging.INFO
    else:
        level = logging.DEBUG

    logging.basicConfig(level=level, format="%(levelname)s: %(message)s")


def vendor_script(path):
    root = os.path.abspath(os.path.join(path, os.pardir))
    if not os.path.isdir(root):
        log.info("Creating directory %s", root)
        os.makedirs(root)

    log.info("Downloading %s to %s", SCRIPT_URL, path)
    urlretrieve(SCRIPT_URL, path)

    log.debug("Making %s executable", path)
    mode = os.stat(path).st_mode
    os.chmod(path, mode | 0o111)


def find_config(root=None, filenames=None, generate=False):
    root = root or os.getcwd()
    filenames = filenames or CONFIG_FILENAMES

    path = None
    log.info("Looking for config file in: %s", root)
    log.debug("Filename options: %s", ", ".join(filenames))
    for filename in os.listdir(root):
        if filename in filenames:
            path = os.path.join(root, filename)
            log.info("Found config file: %s", path)
            return path

    if generate:
        path = generate_config(root, filenames)
        return path

    msg = "No config file found in: {0}".format(root)
    raise RuntimeError(msg)


def generate_config(root=None, filenames=None):
    root = root or os.getcwd()
    filenames = filenames or CONFIG_FILENAMES

    path = os.path.join(root, filenames[0])

    log.info("Generating sample config: %s", path)
    with open(path, 'w') as config:
        config.write(SAMPLE_CONFIG + '\n')

    return path


def parse_config(path):
    data = OrderedDict()  # type: ignore

    log.info("Parsing config file: %s", path)
    config = configparser.ConfigParser()
    config.read(path)

    for section in config.sections():
        data[section] = OrderedDict()
        for name, value in config.items(section):
            data[section][name] = value

    for name in data:
        version = data[name].get('version') or ""
        data[name]['version'] = version
        data[name]['patterns'] = [v.strip() for v in version.split('||')]

    return data


def check_dependencies(config):
    success = []

    for name, settings in config.items():
        show("Checking for {0}...".format(name), head=True)
        output = get_version(settings['cli'], settings.get('cli_version_arg'))

        for pattern in settings['patterns']:
            if match_version(pattern, output):
                show(_("~") + " MATCHED: {0}".format(pattern or "<anything>"))
                success.append(_("~"))
                break
        else:
            if settings.get('optional'):
                show(_("?") + " EXPECTED (OPTIONAL): {0}".format(settings['version']))
                success.append(_("?"))
            else:
                if QUIET:
                    if "not found" in output:
                        actual = "Not found"
                    else:
                        actual = output.split('\n')[0].strip('.')
                    expected = settings['version'] or "<anything>"
                    print("{0}: {1}, EXPECTED: {2}".format(name, actual, expected))
                show(
                    _("x")
                    + " EXPECTED: {0}".format(settings['version'] or "<anything>")
                )
                success.append(_("x"))
            if settings.get('message'):
                show(_("#") + " MESSAGE: {0}".format(settings['message']))

    show("Results: " + " ".join(success), head=True)

    return _("x") not in success


def get_version(program, argument=None):
    if argument is None:
        args = [program, '--version']
    elif argument:
        args = [program, argument]
    else:
        args = [program]

    show("$ {0}".format(" ".join(args)))
    output = call(args)
    lines = output.splitlines()
    show(lines[0] if lines else "<nothing>")

    return output


def match_version(pattern, output):
    if "not found" in output.split('\n')[0]:
        return False

    regex = pattern.replace('.', r'\.') + r'(\b|/)'

    log.debug("Matching %s: %s", regex, output)
    match = re.match(regex, output)
    if match is None:
        match = re.match(r'.*[^\d.]' + regex, output)

    return bool(match)


def call(args):
    try:
        process = Popen(args, stdout=PIPE, stderr=STDOUT)
    except OSError:
        log.debug("Command not found: %s", args[0])
        output = "sh: command not found: {0}".format(args[0])
    else:
        raw = process.communicate()[0]
        output = raw.decode('utf-8').strip()
        log.debug("Command output: %r", output)

    return output


def show(text, start='', end='\n', head=False):
    """Python 2 and 3 compatible version of print."""
    if QUIET:
        return

    if head:
        start = '\n'
        end = '\n\n'

    if log.getEffectiveLevel() < logging.WARNING:
        log.info(text)
    else:
        formatted = start + text + end
        if PY2:
            formatted = formatted.encode('utf-8')
        sys.stdout.write(formatted)
        sys.stdout.flush()


def _(word, is_tty=None, supports_utf8=None, supports_ansi=None):
    """Format and colorize a word based on available encoding."""
    formatted = word

    if is_tty is None:
        is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
    if supports_utf8 is None:
        supports_utf8 = str(sys.stdout.encoding).lower() == 'utf-8'
    if supports_ansi is None:
        supports_ansi = sys.platform != 'win32' or 'ANSICON' in os.environ

    style_support = supports_utf8
    color_support = is_tty and supports_ansi

    if style_support:
        formatted = STYLE.get(word, word)

    if color_support and COLOR.get(word):
        formatted = COLOR[word] + formatted + COLOR[None]

    return formatted


if __name__ == '__main__':  # pragma: no cover
    main()

bin/purge METASEP
#!/usr/bin/env bash

set -ex

if [[ -z "${DOMAIN}" ]]; then
    exit 0
fi

curl -X POST "https://api.cloudflare.com/client/v4/zones/72a69ae7acada4beb0d16053a00560bf/purge_cache" \
	-H "Authorization: Bearer ${CF_API_KEY}" \
	-H "Content-Type: application/json" \
	--data "{\"files\":[ \"https://memegen.link\", \"https://memegen.link/clients/\", \"https://memegen.link/examples/\", \"https://${DOMAIN}\", \"https://${DOMAIN}/docs/swagger.json\", \"https://${DOMAIN}/images\", \"https://${DOMAIN}/examples\", \"https://${DOMAIN}/templates\" ]}"

scripts/simulate_load.py METASEP
"""
poetry run locust -f scripts/simulate_load.py
"""

from random import randint
from urllib.parse import quote

from locust import HttpUser, constant_pacing, task


class Client(HttpUser):

    host = "http://localhost:5000"
    wait_time = constant_pacing(10)

    @task(10)
    def image_from_template(self):
        x = randint(1000000, 9999999)
        path = f"/images/fry/test-{x}.jpg"
        self.client.get(path)

    @task(1)
    def image_from_template_large(self):
        x = randint(1000000, 9999999)
        path = f"/images/fry/test-{x}.png?height=1000"
        self.client.get(path)

    @task(3)
    def image_from_custom(self):
        x = randint(1000000, 9999999)
        background = quote(f"https://memegen.link/img/grid.png?x={x}")
        path = f"/images/custom/test-{x}.png?height=1000&background={background}"
        self.client.get(path)

scripts/check_deployment.py METASEP
import os

import pytest
import requests


@pytest.fixture(scope="session")
def url():
    return os.getenv("SITE", "http://localhost:5000")


def test_get_templates(expect, url):
    response = requests.get(f"{url}/templates")
    expect(response.status_code) == 200


def test_post_images(expect, url):
    params = {"template_id": "iw", "text_lines": ["test", "deployment"]}
    response = requests.post(f"{url}/images", json=params)
    expect(response.status_code) == 201
    expect(response.json()["url"]).endswith("/images/iw/test/deployment.png")


def test_get_examples(expect, url):
    response = requests.get(f"{url}/examples")
    expect(response.status_code) == 200


def test_get_image(expect, url):
    response = requests.get(f"{url}/images/iw/tests_code/in_production.jpg")
    expect(response.status_code) == 200
    expect(response.headers["Content-Type"]) == "image/jpeg"


def test_get_image_custom(expect, url):
    response = requests.get(
        f"{url}/images/custom/test.png"
        "?alt=https://www.gstatic.com/webp/gallery/1.jpg"
    )
    expect(response.status_code) == 200
    expect(response.headers["Content-Type"]) == "image/png"


def test_swagger(expect, url):
    response = requests.get(
        f"https://validator.swagger.io/validator/debug?url="
        f"{url}%2Fdocs%2Fswagger.json"
    )
    expect(response.status_code) == 200
    expect(response.json()) == {}

app/static/robots.txt METASEP
User-agent: Googlebot
Disallow: /

User-agent: *
Allow: /

app/static/favicon.ico METASEP

runtime.txt METASEP
python-3.10.2

requirements.txt METASEP
aiocache==0.11.1
aiofile==3.7.2; python_version >= "3.10" and python_version < "4"
aiofiles==0.7.0; python_version >= "3.7" and python_version < "4.0"
aiohttp==3.7.4.post0; python_version >= "3.6"
aiopath==0.6.10; python_version >= "3.10"
anyio==3.3.2; python_full_version >= "3.6.2" and python_version >= "3.10"
asgiref==3.4.1; python_version >= "3.6"
async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6"
attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
bugsnag==4.1.1; python_version >= "3.5" and python_version < "4"
cached-property==1.5.2; python_version >= "3.7" and python_version < "4.0"
caio==0.9.3; python_version >= "3.10" and python_version < "4"
chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
classproperties==0.2.0; python_version >= "3.7" and python_version < "4.0"
click==8.0.1; python_version >= "3.6"
colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0"
datafiles==1.1.1; python_version >= "3.7" and python_version < "4.0"
furl==2.1.3
gunicorn==20.1.0; python_version >= "3.5"
h11==0.12.0; python_version >= "3.6"
httptools==0.3.0; python_full_version >= "3.5.0" and python_version >= "3.7"
idna==3.2; python_full_version >= "3.6.2" and python_version >= "3.10"
markdown==3.2.2; python_version >= "3.5"
minilog==2.0.1; python_version >= "3.6" and python_version < "4.0"
multidict==5.2.0; python_version >= "3.7"
orderedmultidict==1.0.1
parse==1.19.0; python_version >= "3.7" and python_version < "4.0"
pillow==9.0.0; python_version >= "3.7"
pygments==2.10.0; python_version >= "3.5"
pymdown-extensions==8.2; python_version >= "3.6"
pyperclip==1.8.2
pyyaml==5.4.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0"
ruamel.yaml.clib==0.2.6; platform_python_implementation == "CPython" and python_version < "3.11" and python_version >= "3.7"
ruamel.yaml==0.17.20; python_version >= "3.7" and python_version < "4.0"
sanic-ext==22.1.2
sanic-routing==0.7.1; python_version >= "3.7"
sanic==21.12.1; python_version >= "3.7"
six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.10"
spongemock==0.3.4
tomlkit==0.7.2; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.5.0"
typing-extensions==3.10.0.2; python_version >= "3.7" and python_version < "4.0"
ujson==5.1.0; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.7"
uvicorn==0.14.0
uvloop==0.16.0; sys_platform != "win32" and implementation_name == "cpython" and python_version >= "3.7"
webob==1.8.7; python_version >= "3.5" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "3.5" and python_version < "4" and python_full_version >= "3.3.0"
websockets==10.0; python_version >= "3.7"
yarl==1.7.0; python_version >= "3.6"

pyproject.toml METASEP
[tool.poetry]

name = "memegen"
version = "10.5"
description = "The free and open source API to generate memes."
authors = ["Jace Browning <support@maketested.com>"]
license = "MIT"

packages = [{ include = "app" }]

[tool.poetry.dependencies]

python = "^3.10"

# Sanic
sanic = "~21.12.1"
sanic-ext = "*"

# Markdown
markdown = "~3.2.2"
pymdown-extensions = "^8.0"
pygments = "^2.7.0"

# Images
pillow = "^9.0"
spongemock = "~0.3.4"

# Utilities
aiocache = "~0.11.1"
aiohttp = "^3.7.4"
aiopath = "~0.6.10"
datafiles = "^1.1.1"
furl = "^2.1.3"
minilog = "^2.0.1"

# Production
bugsnag = "^4.0.3"
gunicorn = "^20.1"
uvicorn = "~0.14"

[tool.poetry.dev-dependencies]

# Formatting
isort = "^5.10"
black = "^22.1"

# Type Checking
mypy = "~0.931"
autoflake = "^1.3.1"
types-aiofiles = "*"
types-pkg_resources = "*"
types-requests = "*"

# Testing
pytest = "^6.2.5"
pytest-asyncio = "*"
pytest-describe = "^2.0"
pytest-expecter = "^2.3"
pytest-random = "*"
pytest-rerunfailures = "^9.1"
pytest-cov = "^2.7"
sanic-testing = "^0.8.2"
locust = "^2.5.1"

# Coverage
coveragespace = "^4.2"

# Documentation
mkdocs = "^1.2.3"

# Utilities
honcho = "^1.1"
ipdb = "*"
rope = "^0.18.0"
watchdog = { version = "^2.1.6", extras = ["watchmedo"] }

[tool.black]

quiet = true

[tool.isort]

profile = "black"

[tool.mypy]

ignore_missing_imports = true
no_implicit_optional = true
check_untyped_defs = true

plugins = "datafiles.plugins:mypy"

cache_dir = ".cache/mypy/"

[tool.pytest.ini_options]

testpaths = "app"

addopts = """
--no-header \
--random \
--show-capture=log \
--cov=app --cov-report=term-missing:skip-covered --cov-report=html:.cache/htmlcov --no-cov-on-fail
"""

cache_dir = ".cache"

markers = "slow"

[build-system]

requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

poetry.lock METASEP
[[package]]
name = "aiocache"
version = "0.11.1"
description = "multi backend asyncio cache"
category = "main"
optional = false
python-versions = "*"

[package.extras]
dev = ["asynctest (>=0.11.0)", "codecov", "coverage", "flake8", "ipdb", "marshmallow", "pystache", "pytest", "pytest-asyncio", "pytest-mock", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "black"]
memcached = ["aiomcache (>=0.5.2)"]
msgpack = ["msgpack (>=0.5.5)"]
redis = ["aioredis (>=0.3.3)", "aioredis (>=1.0.0)"]

[[package]]
name = "aiofile"
version = "3.7.2"
description = "Asynchronous file operations."
category = "main"
optional = false
python-versions = ">3.4.*, <4"

[package.dependencies]
caio = ">=0.9.0,<0.10.0"

[package.extras]
develop = ["aiomisc", "asynctest", "pytest", "pytest-cov"]

[[package]]
name = "aiofiles"
version = "0.7.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = ">=3.6,<4.0"

[[package]]
name = "aiohttp"
version = "3.7.4.post0"
description = "Async http client/server framework (asyncio)"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
async-timeout = ">=3.0,<4.0"
attrs = ">=17.3.0"
chardet = ">=2.0,<5.0"
multidict = ">=4.5,<7.0"
typing-extensions = ">=3.6.5"
yarl = ">=1.0,<2.0"

[package.extras]
speedups = ["aiodns", "brotlipy", "cchardet"]

[[package]]
name = "aiopath"
version = "0.6.10"
description = "📁 Async pathlib for Python"
category = "main"
optional = false
python-versions = ">=3.10"

[package.dependencies]
aiofile = ">=3.5.0,<4"
anyio = ">=3.2.0,<4"

[[package]]
name = "anyio"
version = "3.3.2"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.6.2"

[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"

[package.extras]
doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"]

[[package]]
name = "appnope"
version = "0.1.2"
description = "Disable App Nap on macOS >= 10.9"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "asgiref"
version = "3.4.1"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.6"

[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]

[[package]]
name = "async-timeout"
version = "3.0.1"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.5.3"

[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[[package]]
name = "attrs"
version = "21.2.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]

[[package]]
name = "autoflake"
version = "1.4"
description = "Removes unused imports and unused variables"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
pyflakes = ">=1.1.0"

[[package]]
name = "backcall"
version = "0.2.0"
description = "Specifications for callback functions passed in to an API"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "black"
version = "22.1.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"

[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = ">=1.1.0"

[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]

[[package]]
name = "brotli"
version = "1.0.9"
description = "Python bindings for the Brotli compression library"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "bugsnag"
version = "4.1.1"
description = "Automatic error monitoring for django, flask, etc."
category = "main"
optional = false
python-versions = ">=3.5, <4"

[package.dependencies]
webob = "*"

[package.extras]
flask = ["flask", "blinker"]

[[package]]
name = "cached-property"
version = "1.5.2"
description = "A decorator for caching properties in classes."
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "caio"
version = "0.9.3"
description = "Asynchronous file IO for Linux Posix and Windows."
category = "main"
optional = false
python-versions = ">=3.5.*, <4"

[package.extras]
develop = ["aiomisc", "pytest", "pytest-cov"]

[[package]]
name = "certifi"
version = "2021.5.30"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "cffi"
version = "1.14.6"
description = "Foreign Function Interface for Python calling C code."
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
pycparser = "*"

[[package]]
name = "chardet"
version = "4.0.0"
description = "Universal encoding detector for Python 2 and 3"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "charset-normalizer"
version = "2.0.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.5.0"

[package.extras]
unicode_backport = ["unicodedata2"]

[[package]]
name = "classproperties"
version = "0.2.0"
description = "property for class methods"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "click"
version = "8.0.1"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}

[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "configargparse"
version = "1.5.3"
description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[package.extras]
test = ["mock", "pyyaml", "pytest"]
yaml = ["pyyaml"]

[[package]]
name = "coverage"
version = "6.0.1"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
toml = ["tomli"]

[[package]]
name = "coveragespace"
version = "4.2"
description = "A place to track your code coverage metrics."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"

[package.dependencies]
colorama = ">=0.4"
coverage = ">=4.0"
docopt = ">=0.6"
minilog = ">=2.0,<3.0"
requests = ">=2.0,<3.0"

[[package]]
name = "datafiles"
version = "1.1.1"
description = "File-based ORM for dataclasses."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"

[package.dependencies]
cached_property = ">=1.5,<2.0"
classproperties = ">=0.2,<0.3"
minilog = ">=2.0,<3.0"
parse = ">=1.12,<2.0"
"ruamel.yaml" = ">=0.17.20,<0.18.0"
tomlkit = ">=0.7.2,<0.8.0"
typing-extensions = ">=3.7,<4.0"

[[package]]
name = "decorator"
version = "5.1.0"
description = "Decorators for Humans"
category = "dev"
optional = false
python-versions = ">=3.5"

[[package]]
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "flask"
version = "2.0.2"
description = "A simple framework for building complex web applications."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
click = ">=7.1.2"
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.0"

[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]

[[package]]
name = "flask-basicauth"
version = "0.2.0"
description = "HTTP basic access authentication for Flask."
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
Flask = "*"

[[package]]
name = "flask-cors"
version = "3.0.10"
description = "A Flask extension adding a decorator for CORS support"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
Flask = ">=0.9"
Six = "*"

[[package]]
name = "furl"
version = "2.1.3"
description = "URL manipulation made simple."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
orderedmultidict = ">=1.0.1"
six = ">=1.8.0"

[[package]]
name = "gevent"
version = "21.8.0"
description = "Coroutine-based network library"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5"

[package.dependencies]
cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
greenlet = {version = ">=1.1.0,<2.0", markers = "platform_python_implementation == \"CPython\""}
"zope.event" = "*"
"zope.interface" = "*"

[package.extras]
dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"]
docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"]
monitor = ["psutil (>=5.7.0)"]
recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "backports.socketpair", "psutil (>=5.7.0)"]
test = ["requests", "objgraph", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "selectors2", "futures", "mock", "backports.socketpair", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "psutil (>=5.7.0)"]

[[package]]
name = "geventhttpclient"
version = "1.5.2"
description = "http client library for gevent"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
brotli = "*"
certifi = "*"
gevent = ">=0.13"
six = "*"

[[package]]
name = "ghp-import"
version = "2.0.2"
description = "Copy your docs directly to the gh-pages branch."
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
python-dateutil = ">=2.8.1"

[package.extras]
dev = ["twine", "markdown", "flake8", "wheel"]

[[package]]
name = "greenlet"
version = "1.1.2"
description = "Lightweight in-process concurrent programming"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"

[package.extras]
docs = ["sphinx"]

[[package]]
name = "gunicorn"
version = "20.1.0"
description = "WSGI HTTP Server for UNIX"
category = "main"
optional = false
python-versions = ">=3.5"

[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"]
tornado = ["tornado (>=0.2)"]

[[package]]
name = "h11"
version = "0.12.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "honcho"
version = "1.1.0"
description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications."
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}

[package.extras]
export = ["jinja2 (>=2.7,<3)"]

[[package]]
name = "httpcore"
version = "0.13.7"
description = "A minimal low-level HTTP client."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
anyio = ">=3.0.0,<4.0.0"
h11 = ">=0.11,<0.13"
sniffio = ">=1.0.0,<2.0.0"

[package.extras]
http2 = ["h2 (>=3,<5)"]

[[package]]
name = "httptools"
version = "0.3.0"
description = "A collection of framework independent HTTP protocol utils."
category = "main"
optional = false
python-versions = ">=3.5.0"

[package.extras]
test = ["Cython (>=0.29.24,<0.30.0)"]

[[package]]
name = "httpx"
version = "0.18.2"
description = "The next generation HTTP client."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
certifi = "*"
httpcore = ">=0.13.3,<0.14.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
sniffio = "*"

[package.extras]
brotli = ["brotlicffi (>=1.0.0,<2.0.0)"]
http2 = ["h2 (>=3.0.0,<4.0.0)"]

[[package]]
name = "idna"
version = "3.2"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "importlib-metadata"
version = "4.8.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
zipp = ">=0.5"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]

[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "ipdb"
version = "0.13.9"
description = "IPython-enabled pdb"
category = "dev"
optional = false
python-versions = ">=2.7"

[package.dependencies]
decorator = {version = "*", markers = "python_version > \"3.6\""}
ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""}
toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""}

[[package]]
name = "ipython"
version = "7.31.1"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
python-versions = ">=3.7"

[package.dependencies]
appnope = {version = "*", markers = "sys_platform == \"darwin\""}
backcall = "*"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
pygments = "*"
traitlets = ">=4.2"

[package.extras]
all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"]
doc = ["Sphinx (>=1.3)"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["notebook", "ipywidgets"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"]

[[package]]
name = "isort"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.6.1,<4.0"

[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]

[[package]]
name = "itsdangerous"
version = "2.0.1"
description = "Safely pass data to untrusted environments and back."
category = "dev"
optional = false
python-versions = ">=3.6"

[[package]]
name = "jedi"
version = "0.18.0"
description = "An autocompletion tool for Python that can be used for text editors."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
parso = ">=0.8.0,<0.9.0"

[package.extras]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]

[[package]]
name = "jinja2"
version = "3.0.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
MarkupSafe = ">=2.0"

[package.extras]
i18n = ["Babel (>=2.7)"]

[[package]]
name = "locust"
version = "2.5.1"
description = "Developer friendly load testing framework"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
ConfigArgParse = ">=1.0"
flask = ">=2.0.0"
Flask-BasicAuth = ">=0.2.0"
Flask-Cors = ">=3.0.10"
gevent = ">=20.9.0"
geventhttpclient = ">=1.5.1"
msgpack = ">=0.6.2"
psutil = ">=5.6.7"
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
pyzmq = ">=22.2.1"
requests = ">=2.23.0"
roundrobin = ">=0.0.2"
typing-extensions = ">=3.7.4.3"
Werkzeug = ">=2.0.0"

[[package]]
name = "markdown"
version = "3.2.2"
description = "Python implementation of Markdown."
category = "main"
optional = false
python-versions = ">=3.5"

[package.extras]
testing = ["coverage", "pyyaml"]

[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.6"

[[package]]
name = "matplotlib-inline"
version = "0.1.3"
description = "Inline Matplotlib backend for Jupyter"
category = "dev"
optional = false
python-versions = ">=3.5"

[package.dependencies]
traitlets = "*"

[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
category = "dev"
optional = false
python-versions = ">=3.6"

[[package]]
name = "minilog"
version = "2.0.1"
description = "Minimalistic wrapper for Python logging."
category = "main"
optional = false
python-versions = ">=3.6,<4.0"

[[package]]
name = "mkdocs"
version = "1.2.3"
description = "Project documentation with Markdown."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
click = ">=3.3"
ghp-import = ">=1.0"
importlib-metadata = ">=3.10"
Jinja2 = ">=2.10.1"
Markdown = ">=3.2.1"
mergedeep = ">=1.3.4"
packaging = ">=20.5"
PyYAML = ">=3.10"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"

[package.extras]
i18n = ["babel (>=2.9.0)"]

[[package]]
name = "msgpack"
version = "1.0.2"
description = "MessagePack (de)serializer."
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "multidict"
version = "5.2.0"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.6"

[[package]]
name = "mypy"
version = "0.931"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = ">=1.1.0"
typing-extensions = ">=3.10"

[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]

[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "orderedmultidict"
version = "1.0.1"
description = "Ordered Multivalue Dictionary"
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
six = ">=1.8.0"

[[package]]
name = "packaging"
version = "21.0"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
pyparsing = ">=2.0.2"

[[package]]
name = "parse"
version = "1.19.0"
description = "parse() is the opposite of format()"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "parso"
version = "0.8.2"
description = "A Python Parser"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]

[[package]]
name = "pathspec"
version = "0.9.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"

[[package]]
name = "pexpect"
version = "4.8.0"
description = "Pexpect allows easy control of interactive console applications."
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
ptyprocess = ">=0.5"

[[package]]
name = "pickleshare"
version = "0.7.5"
description = "Tiny 'shelve'-like database with concurrency support"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "pillow"
version = "9.0.0"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "platformdirs"
version = "2.4.0"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]

[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]

[[package]]
name = "prompt-toolkit"
version = "3.0.20"
description = "Library for building powerful interactive command lines in Python"
category = "dev"
optional = false
python-versions = ">=3.6.2"

[package.dependencies]
wcwidth = "*"

[[package]]
name = "psutil"
version = "5.8.0"
description = "Cross-platform lib for process and system monitoring in Python."
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.extras]
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]

[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "py"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[[package]]
name = "pycparser"
version = "2.20"
description = "C parser in Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[[package]]
name = "pyflakes"
version = "2.4.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[[package]]
name = "pygments"
version = "2.10.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "pymdown-extensions"
version = "8.2"
description = "Extension pack for Python Markdown."
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
Markdown = ">=3.2"

[[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "pyperclip"
version = "1.8.2"
description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "pytest"
version = "6.2.5"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
toml = "*"

[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]

[[package]]
name = "pytest-asyncio"
version = "0.15.1"
description = "Pytest support for asyncio."
category = "dev"
optional = false
python-versions = ">= 3.6"

[package.dependencies]
pytest = ">=5.4.0"

[package.extras]
testing = ["coverage", "hypothesis (>=5.7.1)"]

[[package]]
name = "pytest-cov"
version = "2.12.1"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[package.dependencies]
coverage = ">=5.2.1"
pytest = ">=4.6"
toml = "*"

[package.extras]
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]

[[package]]
name = "pytest-describe"
version = "2.0.0"
description = "Describe-style plugin for pytest"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
pytest = ">=4.0.0"

[[package]]
name = "pytest-expecter"
version = "2.3"
description = "Better testing with expecter and pytest."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"

[[package]]
name = "pytest-random"
version = "0.02"
description = "py.test plugin to randomize tests"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
pytest = ">=2.2.3"

[[package]]
name = "pytest-rerunfailures"
version = "9.1.1"
description = "pytest plugin to re-run tests to eliminate flaky failures"
category = "dev"
optional = false
python-versions = ">=3.5"

[package.dependencies]
pytest = ">=5.0"

[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"

[package.dependencies]
six = ">=1.5"

[[package]]
name = "pywin32"
version = "301"
description = "Python for Window Extensions"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "pyyaml"
version = "5.4.1"
description = "YAML parser and emitter for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"

[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
pyyaml = "*"

[[package]]
name = "pyzmq"
version = "22.3.0"
description = "Python bindings for 0MQ"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
cffi = {version = "*", markers = "implementation_name == \"pypy\""}
py = {version = "*", markers = "implementation_name == \"pypy\""}

[[package]]
name = "requests"
version = "2.26.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"

[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"

[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]

[[package]]
name = "rfc3986"
version = "1.5.0"
description = "Validating URI References per RFC 3986"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}

[package.extras]
idna2008 = ["idna"]

[[package]]
name = "rope"
version = "0.18.0"
description = "a python refactoring library..."
category = "dev"
optional = false
python-versions = "*"

[package.extras]
dev = ["pytest"]

[[package]]
name = "roundrobin"
version = "0.0.2"
description = "Collection of roundrobin utilities"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "ruamel.yaml"
version = "0.17.20"
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
category = "main"
optional = false
python-versions = ">=3"

[package.dependencies]
"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}

[package.extras]
docs = ["ryd"]
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]

[[package]]
name = "ruamel.yaml.clib"
version = "0.2.6"
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "sanic"
version = "21.12.1"
description = "A web server and web framework that's written to go fast. Build fast. Run fast."
category = "main"
optional = false
python-versions = ">=3.7"

[package.dependencies]
aiofiles = ">=0.6.0"
httptools = ">=0.0.10"
multidict = ">=5.0,<6.0"
sanic-routing = ">=0.7,<1.0"
ujson = {version = ">=1.35", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""}
uvloop = {version = ">=0.5.3", markers = "sys_platform != \"win32\" and implementation_name == \"cpython\""}
websockets = ">=10.0"

[package.extras]
all = ["mistune (<2.0.0)", "flake8", "isort (>=5.0.0)", "sanic-testing (>=0.7.0)", "towncrier", "bandit", "m2r2", "mypy (>=0.901,<0.910)", "pytest-cov", "sphinx (>=2.1.2)", "coverage (==5.3)", "pytest-sugar", "cryptography", "tox", "beautifulsoup4", "black", "pytest-benchmark", "docutils", "gunicorn (==20.0.4)", "pytest-sanic", "uvicorn (<0.15.0)", "pygments", "chardet (>=3.0.0,<4.0.0)", "sphinx-rtd-theme (>=0.4.3)", "pytest (==6.2.5)", "types-ujson"]
dev = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "cryptography", "tox", "towncrier", "types-ujson"]
docs = ["sphinx (>=2.1.2)", "sphinx-rtd-theme (>=0.4.3)", "docutils", "pygments", "m2r2", "mistune (<2.0.0)"]
ext = ["sanic-ext"]
test = ["sanic-testing (>=0.7.0)", "pytest (==6.2.5)", "coverage (==5.3)", "gunicorn (==20.0.4)", "pytest-cov", "beautifulsoup4", "pytest-sanic", "pytest-sugar", "pytest-benchmark", "chardet (>=3.0.0,<4.0.0)", "flake8", "black", "isort (>=5.0.0)", "bandit", "mypy (>=0.901,<0.910)", "docutils", "pygments", "uvicorn (<0.15.0)", "types-ujson"]

[[package]]
name = "sanic-ext"
version = "22.1.2"
description = "Extend your Sanic installation with some core functionality."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
pyyaml = ">=3.0.0"

[package.extras]
dev = ["sanic-testing (>=0.8)", "coverage", "pytest", "pytest-cov", "pytest-asyncio", "tox", "black (>=21.4b2)", "flake8 (>=3.7.7)", "isort (>=5.0.0)"]
test = ["sanic-testing (>=0.8)", "coverage", "pytest", "pytest-cov", "pytest-asyncio", "tox"]

[[package]]
name = "sanic-routing"
version = "0.7.1"
description = "Core routing component for Sanic"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "sanic-testing"
version = "0.8.2"
description = "Core testing clients for Sanic"
category = "dev"
optional = false
python-versions = "*"

[package.dependencies]
httpx = ">=0.18,<0.22"

[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "sniffio"
version = "1.2.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.5"

[[package]]
name = "spongemock"
version = "0.3.4"
description = "Mock some text like spongebob would. mOCk SoMe TexT lIKe SpONGebOb wOuLd."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
pyperclip = ">=1.5.27"

[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"

[[package]]
name = "tomli"
version = "1.2.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.6"

[[package]]
name = "tomlkit"
version = "0.7.2"
description = "Style preserving TOML library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[[package]]
name = "traitlets"
version = "5.1.0"
description = "Traitlets Python configuration system"
category = "dev"
optional = false
python-versions = ">=3.7"

[package.extras]
test = ["pytest"]

[[package]]
name = "types-aiofiles"
version = "0.1.9"
description = "Typing stubs for aiofiles"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "types-pkg-resources"
version = "0.1.3"
description = "Typing stubs for pkg_resources"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "types-requests"
version = "2.25.9"
description = "Typing stubs for requests"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "main"
optional = false
python-versions = "*"

[[package]]
name = "ujson"
version = "5.1.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "urllib3"
version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"

[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]

[[package]]
name = "uvicorn"
version = "0.14.0"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = "*"

[package.dependencies]
asgiref = ">=3.3.4"
click = ">=7"
h11 = ">=0.8"

[package.extras]
standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]

[[package]]
name = "uvloop"
version = "0.16.0"
description = "Fast implementation of asyncio event loop on top of libuv"
category = "main"
optional = false
python-versions = ">=3.7"

[package.extras]
dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]

[[package]]
name = "watchdog"
version = "2.1.6"
description = "Filesystem events monitoring"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.dependencies]
PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"watchmedo\""}

[package.extras]
watchmedo = ["PyYAML (>=3.10)"]

[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"

[[package]]
name = "webob"
version = "1.8.7"
description = "WSGI request and response object"
category = "main"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"

[package.extras]
docs = ["Sphinx (>=1.7.5)", "pylons-sphinx-themes"]
testing = ["pytest (>=3.1.0)", "coverage", "pytest-cov", "pytest-xdist"]

[[package]]
name = "websockets"
version = "10.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
category = "main"
optional = false
python-versions = ">=3.7"

[[package]]
name = "werkzeug"
version = "2.0.2"
description = "The comprehensive WSGI web application library."
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
watchdog = ["watchdog"]

[[package]]
name = "yarl"
version = "1.7.0"
description = "Yet another URL library"
category = "main"
optional = false
python-versions = ">=3.6"

[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"

[[package]]
name = "zipp"
version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
python-versions = ">=3.6"

[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]

[[package]]
name = "zope.event"
version = "4.5.0"
description = "Very basic event publishing system"
category = "dev"
optional = false
python-versions = "*"

[package.extras]
docs = ["sphinx"]
test = ["zope.testrunner"]

[[package]]
name = "zope.interface"
version = "5.4.0"
description = "Interfaces for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"

[package.extras]
docs = ["sphinx", "repoze.sphinx.autointerface"]
test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]

[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "984f1215a75534aed6e32db25f711827e99f24492b3224ae1a6135d0cca91cb4"

[metadata.files]
aiocache = [
    {file = "aiocache-0.11.1-py2.py3-none-any.whl", hash = "sha256:e55c7caaa5753794fd301c3a2e592737fa1d036db9f8d04ae154facdfb48a157"},
    {file = "aiocache-0.11.1.tar.gz", hash = "sha256:f2ebe0b05cec45782e7b5ea0bb74640f157dd4bb1028b4565364dda9fe33be7f"},
]
aiofile = [
    {file = "aiofile-3.7.2-py3-none-any.whl", hash = "sha256:b2a153a2f8688c9e7c2df00f26024cc4ae03ad2de15aa41a68d939b46eaad859"},
    {file = "aiofile-3.7.2.tar.gz", hash = "sha256:e1c577ab033cf1fd01ff33e033431223c5310254cc41bc3c6bf467e702a29791"},
]
aiofiles = [
    {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"},
    {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"},
]
aiohttp = [
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"},
    {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"},
    {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"},
    {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"},
    {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"},
    {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"},
]
aiopath = [
    {file = "aiopath-0.6.10-py2.py3-none-any.whl", hash = "sha256:44a5376de098b4875eefa4b4599f5ac99fd9451b88c314f237716571e16ccce2"},
    {file = "aiopath-0.6.10.tar.gz", hash = "sha256:6ca97a864fe32478d53af6c40e1951de64ab01d6b402d88524ce72d80362eb7a"},
]
anyio = [
    {file = "anyio-3.3.2-py3-none-any.whl", hash = "sha256:c32da314c510b34a862f5afeaf8a446ffed2c2fde21583e654bd71ecfb5b744b"},
    {file = "anyio-3.3.2.tar.gz", hash = "sha256:0b993a2ef6c1dc456815c2b5ca2819f382f20af98087cc2090a4afed3a501436"},
]
appnope = [
    {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
    {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
]
asgiref = [
    {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"},
    {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"},
]
async-timeout = [
    {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
    {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
]
atomicwrites = [
    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
autoflake = [
    {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"},
]
backcall = [
    {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
    {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
black = [
    {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"},
    {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"},
    {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"},
    {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"},
    {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"},
    {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"},
    {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"},
    {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"},
    {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"},
    {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"},
    {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"},
    {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"},
    {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"},
    {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"},
    {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"},
    {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"},
    {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"},
    {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"},
    {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"},
    {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"},
    {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"},
    {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"},
    {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"},
]
brotli = [
    {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
    {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
    {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
    {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
    {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
    {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
    {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
    {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
    {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
    {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
    {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
    {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
    {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
    {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
    {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
    {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
    {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
    {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
    {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
    {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
    {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
    {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
    {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
    {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
    {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
    {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
    {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
    {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
    {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
    {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
    {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
    {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
    {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
    {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
    {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
    {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
    {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
    {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
    {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
    {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
    {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
    {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
    {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
]
bugsnag = [
    {file = "bugsnag-4.1.1-py3-none-any.whl", hash = "sha256:03a3c39549fa248842a53b6d195fa3d64f5906aff4e2eeba98aadd033e1da968"},
    {file = "bugsnag-4.1.1.tar.gz", hash = "sha256:cdbdb3e02ef0c0655bb55be8b05ec1cb830b5ec629923ccb24bfd71dede3d1c3"},
]
cached-property = [
    {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
    {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
]
caio = [
    {file = "caio-0.9.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:966575da339c004c2410b4a92a6b7639659a6e90f45849dafa839e9dd2177c77"},
    {file = "caio-0.9.3-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:46f649cbd68c6762e0a2d6fc1ab69ab9b06ddcdaab52241514b8c7cce48be08a"},
    {file = "caio-0.9.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cccd691fa9eadb378ec2baf6bce6427640a629426a4dcd56d66b849a09c49949"},
    {file = "caio-0.9.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7331c49c6d9036a72ba9ae17de9804359b84c0c3c69bb9f3ab7020bf47f59a53"},
    {file = "caio-0.9.3-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:e475ed5cadd550aef36b10a74613fa797e8141cd7360af71507ed29eaaf20b0b"},
    {file = "caio-0.9.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa4b8f9538a2b52081edee7f972f6644f02149ae7d1ea9c3a7d55df9c1cc182f"},
    {file = "caio-0.9.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ab94072fb0b00ee0108acd938c9444b828bd9dd0afbb54059293c1e5e61f2a76"},
    {file = "caio-0.9.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c10054e6bc19e619033bb6f6ef8000b334596f0a947de91462513e5f54054db"},
    {file = "caio-0.9.3-py3-none-any.whl", hash = "sha256:fae7cd90da87effda72ec2ca85df67f6dc91e58c0dfb6244db4c4d32d1a43fb7"},
    {file = "caio-0.9.3.tar.gz", hash = "sha256:2e46b0c008b8baadcac9c8acc66aa1fff58acae779a8a81479795a499f3a6a40"},
]
certifi = [
    {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
    {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
cffi = [
    {file = "cffi-1.14.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c"},
    {file = "cffi-1.14.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99"},
    {file = "cffi-1.14.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819"},
    {file = "cffi-1.14.6-cp27-cp27m-win32.whl", hash = "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20"},
    {file = "cffi-1.14.6-cp27-cp27m-win_amd64.whl", hash = "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224"},
    {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7"},
    {file = "cffi-1.14.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33"},
    {file = "cffi-1.14.6-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534"},
    {file = "cffi-1.14.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a"},
    {file = "cffi-1.14.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5"},
    {file = "cffi-1.14.6-cp35-cp35m-win32.whl", hash = "sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca"},
    {file = "cffi-1.14.6-cp35-cp35m-win_amd64.whl", hash = "sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218"},
    {file = "cffi-1.14.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f"},
    {file = "cffi-1.14.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872"},
    {file = "cffi-1.14.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195"},
    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d"},
    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b"},
    {file = "cffi-1.14.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb"},
    {file = "cffi-1.14.6-cp36-cp36m-win32.whl", hash = "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a"},
    {file = "cffi-1.14.6-cp36-cp36m-win_amd64.whl", hash = "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e"},
    {file = "cffi-1.14.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5"},
    {file = "cffi-1.14.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf"},
    {file = "cffi-1.14.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"},
    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56"},
    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c"},
    {file = "cffi-1.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762"},
    {file = "cffi-1.14.6-cp37-cp37m-win32.whl", hash = "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771"},
    {file = "cffi-1.14.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a"},
    {file = "cffi-1.14.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0"},
    {file = "cffi-1.14.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e"},
    {file = "cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346"},
    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc"},
    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd"},
    {file = "cffi-1.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc"},
    {file = "cffi-1.14.6-cp38-cp38-win32.whl", hash = "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548"},
    {file = "cffi-1.14.6-cp38-cp38-win_amd64.whl", hash = "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156"},
    {file = "cffi-1.14.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d"},
    {file = "cffi-1.14.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e"},
    {file = "cffi-1.14.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c"},
    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202"},
    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f"},
    {file = "cffi-1.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87"},
    {file = "cffi-1.14.6-cp39-cp39-win32.whl", hash = "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728"},
    {file = "cffi-1.14.6-cp39-cp39-win_amd64.whl", hash = "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2"},
    {file = "cffi-1.14.6.tar.gz", hash = "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd"},
]
chardet = [
    {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
charset-normalizer = [
    {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
    {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
]
classproperties = [
    {file = "classproperties-0.2.0.tar.gz", hash = "sha256:a77e96a666898ecd697dd7f331d9ab1e38383c1edef809d953bb5f8a3ea67c2a"},
]
click = [
    {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
    {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
configargparse = [
    {file = "ConfigArgParse-1.5.3-py3-none-any.whl", hash = "sha256:18f6535a2db9f6e02bd5626cc7455eac3e96b9ab3d969d366f9aafd5c5c00fe7"},
    {file = "ConfigArgParse-1.5.3.tar.gz", hash = "sha256:1b0b3cbf664ab59dada57123c81eff3d9737e0d11d8cf79e3d6eb10823f1739f"},
]
coverage = [
    {file = "coverage-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:abe8207dfb8a61ded9cd830d26c1073c8218fc0ae17eb899cfe8ec0fafae6e22"},
    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83faa3692e8306b20293889714fdf573d10ef5efc5843bd7c7aea6971487bd6a"},
    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f82a17f2a77958f3eef40ad385fc82d4c6ba9a77a51a174efe03ce75daebbc16"},
    {file = "coverage-6.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b06f4f1729e2963281d9cd6e65e6976bf27b44d4c07ac5b47223ce45f822cec"},
    {file = "coverage-6.0.1-cp310-cp310-win32.whl", hash = "sha256:7600fac458f74c68b097379f76f3a6e3a630493fc7fc94b6508fedd9d498c194"},
    {file = "coverage-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c5f39d1556e75fc3c4fb071f9e7cfa618895a999a0de763a541d730775d0d5f"},
    {file = "coverage-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3edbb3ec580c73e5a264f5d04f30245bc98eff1a26765d46c5c65134f0a0e2f7"},
    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea452a2d83964d08232ade470091015e7ab9b8f53acbec10f2210fbab4ce7e43"},
    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1770d24f45f1f2daeae34cfa3b33fcb29702153544cd2ad40d58399dd4ff53b5"},
    {file = "coverage-6.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ad7182a82843f9f85487f44567c8c688f16c906bdb8d0e44ae462aed61cb8f1b"},
    {file = "coverage-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:9d242a2434801ef5125330deddb4cddba8990c9a49b3dec99dca17dd7eefba5a"},
    {file = "coverage-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e66c50f0ab445fec920a9f084914ea1776a809e3016c3738519048195f851bbb"},
    {file = "coverage-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1ceacb86e0a9558061dcc6baae865ed25933ea57effea644f21657cdce19bc"},
    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e15ab5afbee34abf716fece80ea33ea09a82e7450512f022723b1a82ec9a4e"},
    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6873f3f954d3e3ab8b1881f4e5307cc19f70c9f931c41048d9f7e6fd946eabe7"},
    {file = "coverage-6.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0898d6948b31df13391cd40568de8f35fa5901bc922c5ae05cf070587cb9c666"},
    {file = "coverage-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9c416ba03844608f45661a5b48dc59c6b5e89956efe388564dd138ca8caf540b"},
    {file = "coverage-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:66fe33e9e0df58675e08e83fe257f89e7f625e7633ea93d0872154e09cce2724"},
    {file = "coverage-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:353a50f123f0185cdb7a1e1e3e2cfb9d1fd7e293cfaf68eedaf5bd8e02e3ec32"},
    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b81a4e667c45b13658b84f9b8f1d32ef86d5405fabcbd181b76b9e51d295f397"},
    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17426808e8e0824f864876312d41961223bf5e503bf8f1f846735279a60ea345"},
    {file = "coverage-6.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e11cca9eb5c9b3eaad899728ee2ce916138399ee8cbbccaadc1871fecb750827"},
    {file = "coverage-6.0.1-cp38-cp38-win32.whl", hash = "sha256:0a7e55cc9f7efa22d5cc9966276ec7a40a8803676f6ccbfdc06a486fba9aa9ee"},
    {file = "coverage-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:4eb9cd910ca8e243f930243a9940ea1a522e32435d15668445753d087c30ee12"},
    {file = "coverage-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83682b73785d2e078e0b5f63410b8125b122e1a22422640c57edd4011c950f3e"},
    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b45f89a8ef65c29195f8f28dbe215f44ccb29d934f3e862d2a5c12e38698a793"},
    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:73880a80fad0597eca43e213e5e1711bf6c0fcdb7eb6b01b3b17841ebe5a7f8d"},
    {file = "coverage-6.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f398d38e6ebc2637863db1d7be3d4f9c5174e7d24bb3b0716cdb1f204669cbcf"},
    {file = "coverage-6.0.1-cp39-cp39-win32.whl", hash = "sha256:1864bdf9b2ccb43e724051bc23a1c558daf101ad4488ede1945f2a8be1facdad"},
    {file = "coverage-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:c9c413c4397d4cdc7ca89286158d240ce524f9667b52c9a64dd7e13d16cf8815"},
    {file = "coverage-6.0.1-pp36-none-any.whl", hash = "sha256:65da6e3e8325291f012921bbf71fea0a97824e1c573981871096aac6e2cf0ec5"},
    {file = "coverage-6.0.1-pp37-none-any.whl", hash = "sha256:07efe1fbd72e67df026ad5109bcd216acbbd4a29d5208b3dab61779bae6b7b26"},
    {file = "coverage-6.0.1.tar.gz", hash = "sha256:3490ff6dbf3f7accf0750136ed60ae1f487bccc1f097740e3b21262bc9c89854"},
]
coveragespace = [
    {file = "coveragespace-4.2-py3-none-any.whl", hash = "sha256:3b8651c97b0741f89786eb57fb0b27a606502207439825d9f1c6f99b0e598e68"},
    {file = "coveragespace-4.2.tar.gz", hash = "sha256:b23263b7b9689fd7ded975809b134453c0e0cdbf1dea6b60bf8ebed2c78f9270"},
]
datafiles = [
    {file = "datafiles-1.1.1-py3-none-any.whl", hash = "sha256:e737cf927439767798df991ae7689324e42c7fb8d8b9ffd601ffcad34e857529"},
    {file = "datafiles-1.1.1.tar.gz", hash = "sha256:16f15323cdcd6707a875b4956c8660e8c62cd1e2a775c02c5dde79bcba88a283"},
]
decorator = [
    {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
    {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
]
docopt = [
    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
flask = [
    {file = "Flask-2.0.2-py3-none-any.whl", hash = "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a"},
    {file = "Flask-2.0.2.tar.gz", hash = "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2"},
]
flask-basicauth = [
    {file = "Flask-BasicAuth-0.2.0.tar.gz", hash = "sha256:df5ebd489dc0914c224419da059d991eb72988a01cdd4b956d52932ce7d501ff"},
]
flask-cors = [
    {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"},
    {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"},
]
furl = [
    {file = "furl-2.1.3-py2.py3-none-any.whl", hash = "sha256:9ab425062c4217f9802508e45feb4a83e54324273ac4b202f1850363309666c0"},
    {file = "furl-2.1.3.tar.gz", hash = "sha256:5a6188fe2666c484a12159c18be97a1977a71d632ef5bb867ef15f54af39cc4e"},
]
gevent = [
    {file = "gevent-21.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e91632fdcf1c9a33e97e35f96edcbdf0b10e36cf53b58caa946dca4836bb688c"},
    {file = "gevent-21.8.0-cp27-cp27m-win32.whl", hash = "sha256:84e1af2dfb4ea9495cb914b00b6303ca0d54bf0a92e688a17e60f6b033873df2"},
    {file = "gevent-21.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:3b600145dc0c5b39c6f89c2e91ec6c55eb0dd52dc8148228479ca42cded358e4"},
    {file = "gevent-21.8.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:90131877d3ce1a05da1b718631860815b89ff44e93c42d168c9c9e8893b26318"},
    {file = "gevent-21.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:3f5ba654bdd3c774079b553fef535ede5b52c7abd224cb235a15da90ae36251b"},
    {file = "gevent-21.8.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02d1e8ca227d0ab0b7917fd7e411f9a534475e0a41fb6f434e9264b20155201a"},
    {file = "gevent-21.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55ede95f41b74e7506fab293ad04cc7fc2b6f662b42281e9f2d668ad3817b574"},
    {file = "gevent-21.8.0-cp36-cp36m-win32.whl", hash = "sha256:72d4c2a8e65bbc702db76456841c7ddd6de2d9ab544a24aa74ad9c2b6411a269"},
    {file = "gevent-21.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ceec7c5f15fb2f9b767b194daa55246830db6c7c3c2f0b1c7e9e90cb4d01f3f9"},
    {file = "gevent-21.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:6cad37a55e904879beef2a7e7c57c57d62fde2331fef1bec7f2b2a7ef14da6a2"},
    {file = "gevent-21.8.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaf1451cd0d9c32f65a50e461084a0540be52b8ea05c18669c95b42e1f71592a"},
    {file = "gevent-21.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c7b4763514fec74c9fe6ad10c3de62d8fe7b926d520b1e35eb6887181b954ff"},
    {file = "gevent-21.8.0-cp37-cp37m-win32.whl", hash = "sha256:1c9c87b15f792af80edc950a83ab8ef4f3ba3889712211c2c42740ddb57b5492"},
    {file = "gevent-21.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:23077d87d1589ac141c22923fd76853d2cc5b7e3c5e1f1f9cdf6ff23bc9790fc"},
    {file = "gevent-21.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:4c94d27be9f0439b28eb8bd0f879e6142918c62092fda7fb96b6d06f01886b94"},
    {file = "gevent-21.8.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8d8655ce581368b7e1ab42c8a3a166c0b43ea04e59970efbade9448864585e99"},
    {file = "gevent-21.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f918bdf7d2096e391f66bd8ce1e969639aa235e710aaf750a37774bb585bd"},
    {file = "gevent-21.8.0-cp38-cp38-win32.whl", hash = "sha256:f39d5defda9443b5fb99a185050e94782fe7ac38f34f751b491142216ad23bc7"},
    {file = "gevent-21.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afc877ff4f277d0e51a1206d748fdab8c1e0256f7a05e1b1067abbed71c64da9"},
    {file = "gevent-21.8.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a8656d6e02bf47d7fa47728cf7a7cbf408f77ef1fad12afd9e0e3246c5de1707"},
    {file = "gevent-21.8.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9d46bea8644048ceac5737950c08fc89c37a66c34a56a6c9e3648726e60cb767"},
    {file = "gevent-21.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75c29ed5148c916021d39d2fac90ccc0e19adf854626a34eaee012aa6b1fcb67"},
    {file = "gevent-21.8.0-cp39-cp39-win32.whl", hash = "sha256:e00dc0450f79253b7a3a7f2a28e6ca959c8d0d47c0f9fa2c57894c7974d5965f"},
    {file = "gevent-21.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:37a469a99e6000b42dd0b9bbd9d716dbd66cdc6e5738f136f6a266c29b90ee99"},
    {file = "gevent-21.8.0-pp27-pypy_73-win32.whl", hash = "sha256:b10c3326edb76ec3049646dc5131608d6d3733b5adfc75d34852028ecc67c52c"},
    {file = "gevent-21.8.0.tar.gz", hash = "sha256:43e93e1a4738c922a2416baf33f0afb0a20b22d3dba886720bc037cd02a98575"},
]
geventhttpclient = [
    {file = "geventhttpclient-1.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4d299aca6661e6dc5fdd0b61e36cb56b57c100b32160368acfb01d107537d4d"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4efb28deaaa5a1ee340adc6092bedf1a8c8c84a652c6999a6e2a740a5564290d"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:333c58dbb629eb78a18a5338b672be4a1811f710bb6fd13558530515e73b7054"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92b5c62f2bd6341ea67663c3f3c8294871e14ffeb4d8bfd95d18c68d0a8f3ae0"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c719ef2af3712db58f3b51c5ccb17aa89534c2c256711ecc3c3c54693d9b5e4"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:10e0473981f93b13889d32848a5d155662baaa3b123237f52172dba3126bde6a"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-win32.whl", hash = "sha256:9c9cc12ed0c68e1896f6f971dd5dcc1800d0f3eb8487fbc44350a962d0b3bddf"},
    {file = "geventhttpclient-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:ab6bbb1d8746a40db01b9dda0792763180d10aa462134c81f4bf78c93b47d6b7"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdc99373df33880a5ec9678301174ef86a57f22f8a550539af7d2080852b761d"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cda5792b65c738407f65bbc927d6d5a4a0e25ae7490c58d7afac26630471090"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e9ce8167d2d2da80c89355ba4ef51a3dbb0511f724ad69c65cf896add9ca6b00"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38c18895d3b0c51eb30da544912175d8760a269e45a3c639b89f00fc45635514"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-win32.whl", hash = "sha256:477ceea2a7c36aaf95cf4428b8ffc8c9b60dc1ced143332fb814b57116ae2942"},
    {file = "geventhttpclient-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:3411274b1a465fc7139d6ee2203145cf8db7ea6af720c8c829bed851c86abdeb"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e935bd4e66af2f196a99a2f42d8e6d054c6dd747f979d9310a6f0c0c01ff737"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba375fc918a9aac0d305107bd954b1db141907116a3abcb40bbdddc5f326343c"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6e5bcc99f311107b16c9af3f9dc3cfb8c09e85e27f2bb7bb7bd641dcd6e09187"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:342b2bcc13576c59d96b8eb21b8d0a49a2593e0914cfe20c55289ade1445fb19"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-win32.whl", hash = "sha256:24a59119b84dc79d4d5f8d7cc1c150949a8b2182abe035cfd87fa8055fe53207"},
    {file = "geventhttpclient-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e698e209b5e2bf805c4e7ae43f55abd104c1a0654a5b652a917b1b0f5527dd21"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb599c5650066ce23a8a07d6c8d6ed1ec69faf1a34bf99e29ace23bc95ac153"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ea8b971d27d1f029dc0a6dd108462b79433358377757d38d26ede0e60216d14"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:58b91d2120ea3d20ad5f2da8ac74b1ca41d8991bcef81137a5ba55cd23803ae9"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d98a2e1dda645562cd979e710bdcc0bb43a32316cf334eeeb1626fa27b2bf55"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ece3dc7287c66efb1f134b35574d63afff23ef8ff4b657f2a958952e96d0d9c3"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44b069ec44c7453a4b9e9170d14a2b763f674d20fe522a0663b534d3176bb27d"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-win32.whl", hash = "sha256:1ab631f5b863399ed9928be7bc1b640751e2018e4e59aee322cdfa5f63479462"},
    {file = "geventhttpclient-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:722e61aba464525b4a91c397426593ed9d6c2303d3b0550af2c872f43ce1eddd"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e8a148d0d0226dcbd5a272b327164a32d4b76ea0ac70417d7f8b7b5ad0324485"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8835803789b5ef012b180ee057a02e7f3bb8a143671c266efe2f091cefe895e7"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:065e809e4422da9bb9b03f4d03ddb1eb0314706057435830d072af1f61a91a66"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd1dd3228367a142285b76d8aea2a37f321605bc7fe7e50855ef699e4d9a246d"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ca7440e8a448c15222858ea7bfd5c376bfc3f17e1684134589c864c5cf51f0b"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d57b5ae8b364250bfb8ef721cc721b8f5f4d9cb586768e4ea920076447fe71e"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-win32.whl", hash = "sha256:37209d9cc4062b39a3aa6932b955b53b86dae578679dc29956f71a36b9d01c37"},
    {file = "geventhttpclient-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:890616c4389cf3b3f67e8e5e455c97c2a8a40d8fdacdc0c0f2bc491e82f42aea"},
    {file = "geventhttpclient-1.5.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bb3abf203afd7b51ab1c17a16f9b77f24a556d5d640b5c60afa55d8712351a34"},
    {file = "geventhttpclient-1.5.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:128b3f184eb3b0a1c02ad41e125b4701e21278ae27828dad55548e9615eae1a7"},
    {file = "geventhttpclient-1.5.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:74f0f6305c4e5ba4539bbc1309c776683bdd44beb443f7db6f67e8ca63cb1a60"},
    {file = "geventhttpclient-1.5.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15de435c0fc8afa062603dea0688c7093af7acee680f6f2cf2256ec2ea3c2026"},
    {file = "geventhttpclient-1.5.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6c64ec9af8f023d82f336addc4d81e5209136fb5a16547be64b4055e90aeadb5"},
    {file = "geventhttpclient-1.5.2.tar.gz", hash = "sha256:f3c5884d62be75162426728e3d86788740b13e363b197ef592f1443dba240709"},
]
ghp-import = [
    {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"},
    {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"},
]
greenlet = [
    {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
    {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"},
    {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"},
    {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"},
    {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"},
    {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"},
    {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"},
    {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"},
    {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"},
    {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"},
    {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"},
    {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"},
    {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"},
    {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"},
    {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"},
    {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"},
    {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"},
    {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"},
    {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"},
    {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"},
    {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"},
    {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"},
    {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"},
    {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"},
    {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"},
    {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"},
    {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"},
    {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"},
    {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"},
    {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"},
    {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"},
    {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"},
    {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"},
    {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"},
    {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"},
    {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"},
    {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"},
    {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"},
    {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"},
    {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"},
    {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"},
    {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"},
    {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"},
    {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"},
    {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"},
    {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"},
    {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"},
    {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"},
    {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"},
    {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"},
    {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"},
    {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"},
    {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"},
    {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"},
    {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"},
]
gunicorn = [
    {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
    {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
]
h11 = [
    {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
    {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
]
honcho = [
    {file = "honcho-1.1.0-py2.py3-none-any.whl", hash = "sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f"},
    {file = "honcho-1.1.0.tar.gz", hash = "sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"},
]
httpcore = [
    {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"},
    {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"},
]
httptools = [
    {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4137137de8976511a392e27bfdcf231bd926ac13d375e0414e927b08217d779e"},
    {file = "httptools-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9f475b642c48b1b78584bdd12a5143e2c512485664331eade9c29ef769a17598"},
    {file = "httptools-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4687dfc116a9f1eb22a7d797f0dc6f6e17190d406ca4e729634b38aa98044b17"},
    {file = "httptools-0.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:72ee0e3fb9c6437ab3ae34e9abee67fcee6876f4f58504e3f613dd5882aafdb7"},
    {file = "httptools-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:3787c1f46e9722ef7f07ea5c76b0103037483d1b12e34a02c53ceca5afa4e09a"},
    {file = "httptools-0.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c0ac2e0ce6733c55858932e7d37fcc7b67ba6bb23e9648593c55f663de031b93"},
    {file = "httptools-0.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79717080dc3f8b1eeb7f820b9b81528acbc04be6041f323fdd97550da2062575"},
    {file = "httptools-0.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eda95634027200f4b2a6d499e7c2e7fa9b8ee57e045dfda26958ea0af27c070b"},
    {file = "httptools-0.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3f82eb106e1474c63dba36a176067e65b48385f4cecddf3616411aa5d1fbdfec"},
    {file = "httptools-0.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c14576b737d9e6e4f2a86af04918dbe9b62f57ce8102a8695c9a382dbe405c7f"},
    {file = "httptools-0.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:113816f9af7dcfc4aa71ebb5354d77365f666ecf96ac7ff2aa1d24b6bca44165"},
    {file = "httptools-0.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b8ac7dee63af4346e02b1e6d32202e3b5b3706a9928bec6da6d7a5b066217422"},
    {file = "httptools-0.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:04114db99605c9b56ea22a8ec4d7b1485b908128ed4f4a8f6438489c428da794"},
    {file = "httptools-0.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6e676bc3bb911b11f3d7e2144b9a53600bf6b9b21e0e4437aa308e1eef094d97"},
    {file = "httptools-0.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdc3975db86c29817e6d13df14e037c931fc893a710fb71097777a4147090068"},
    {file = "httptools-0.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ac842df4fc3952efa7820b277961ea55e068bbc54cb59a0820400de7ae358d8"},
    {file = "httptools-0.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:47dba2345aaa01b87e4981e8756af441349340708d5b60712c98c55a4d28f4af"},
    {file = "httptools-0.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:5a836bd85ae1fb4304f674808488dae403e136d274aa5bafd0e6ee456f11c371"},
    {file = "httptools-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1a8f26327023fa1a947d36e60a0582149e182fbbc949c8a65ec8665754dbbe69"},
    {file = "httptools-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a10a5903b5bc0eb647d01cd1e95bec3bb614a9bf53f0af1e01360b2debdf81"},
    {file = "httptools-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21e948034f70e47c8abfa2d5e6f1a5661f87a2cddc7bcc70f61579cc87897c70"},
    {file = "httptools-0.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:074afd8afdeec0fa6786cd4a1676e0c0be23dc9a017a86647efa6b695168104f"},
    {file = "httptools-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:2119fa619a4c53311f594f25c0205d619350fcb32140ec5057f861952e9b2b4f"},
    {file = "httptools-0.3.0.tar.gz", hash = "sha256:3f9b4856d46ba1f0c850f4e84b264a9a8b4460acb20e865ec00978ad9fbaa4cf"},
]
httpx = [
    {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"},
    {file = "httpx-0.18.2.tar.gz", hash = "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6"},
]
idna = [
    {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
    {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
importlib-metadata = [
    {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
    {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
iniconfig = [
    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipdb = [
    {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
]
ipython = [
    {file = "ipython-7.31.1-py3-none-any.whl", hash = "sha256:55df3e0bd0f94e715abd968bedd89d4e8a7bce4bf498fb123fed4f5398fea874"},
    {file = "ipython-7.31.1.tar.gz", hash = "sha256:b5548ec5329a4bcf054a5deed5099b0f9622eb9ea51aaa7104d215fece201d8c"},
]
isort = [
    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
itsdangerous = [
    {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"},
    {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"},
]
jedi = [
    {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"},
    {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
]
jinja2 = [
    {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"},
    {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"},
]
locust = [
    {file = "locust-2.5.1-py3-none-any.whl", hash = "sha256:8527e0c4eff0b27f2368a96bd6036a379049d3efd17b7750ea1a54d5e23017fe"},
    {file = "locust-2.5.1.tar.gz", hash = "sha256:da04bcf2248d07e47a05a3997ffc54aaf3215f7f4ca87acf5b7ec65bbef92694"},
]
markdown = [
    {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"},
    {file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"},
]
markupsafe = [
    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
    {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
    {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
    {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
    {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
    {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
    {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
matplotlib-inline = [
    {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
    {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
]
mergedeep = [
    {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
    {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
minilog = [
    {file = "minilog-2.0.1-py3-none-any.whl", hash = "sha256:9cd8b002043c7a6e5f312af96b06f562932312217d4fcb8c2752edc1ae91d3c7"},
    {file = "minilog-2.0.1.tar.gz", hash = "sha256:52cbec563f266f1e931fd884edbb53a87ce24817365d84cc9dc628a96ea00b85"},
]
mkdocs = [
    {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"},
    {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"},
]
msgpack = [
    {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9"},
    {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192"},
    {file = "msgpack-1.0.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841"},
    {file = "msgpack-1.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"},
    {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326"},
    {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439"},
    {file = "msgpack-1.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f"},
    {file = "msgpack-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2"},
    {file = "msgpack-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc"},
    {file = "msgpack-1.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54"},
    {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87"},
    {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c"},
    {file = "msgpack-1.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1"},
    {file = "msgpack-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a"},
    {file = "msgpack-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b"},
    {file = "msgpack-1.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06"},
    {file = "msgpack-1.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c"},
    {file = "msgpack-1.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e"},
    {file = "msgpack-1.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83"},
    {file = "msgpack-1.0.2-cp38-cp38-win32.whl", hash = "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9"},
    {file = "msgpack-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009"},
    {file = "msgpack-1.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694"},
    {file = "msgpack-1.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759"},
    {file = "msgpack-1.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887"},
    {file = "msgpack-1.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e"},
    {file = "msgpack-1.0.2-cp39-cp39-win32.whl", hash = "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33"},
    {file = "msgpack-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f"},
    {file = "msgpack-1.0.2.tar.gz", hash = "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984"},
]
multidict = [
    {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"},
    {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"},
    {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"},
    {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"},
    {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"},
    {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"},
    {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"},
    {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"},
    {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"},
    {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"},
    {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"},
    {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"},
    {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"},
    {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"},
    {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"},
    {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"},
    {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"},
    {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"},
    {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"},
    {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"},
    {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"},
    {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"},
    {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"},
    {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"},
    {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"},
    {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"},
    {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"},
    {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"},
    {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"},
    {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"},
    {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"},
    {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"},
    {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"},
    {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"},
    {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"},
    {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"},
    {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"},
    {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"},
    {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"},
    {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"},
    {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"},
    {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"},
    {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"},
    {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"},
    {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"},
    {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"},
    {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"},
    {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"},
    {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"},
    {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"},
    {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"},
    {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"},
    {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"},
    {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"},
    {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"},
    {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"},
    {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"},
    {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"},
    {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"},
    {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"},
    {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"},
    {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"},
    {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"},
    {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"},
    {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"},
    {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"},
    {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"},
    {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"},
    {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"},
    {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"},
    {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"},
    {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"},
]
mypy = [
    {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
    {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
    {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
    {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
    {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
    {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
    {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
    {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
    {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
    {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
    {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
    {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
    {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
    {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
    {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
    {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
    {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
    {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
    {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
    {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
]
mypy-extensions = [
    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
orderedmultidict = [
    {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"},
    {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"},
]
packaging = [
    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
parse = [
    {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"},
]
parso = [
    {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
    {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"},
]
pathspec = [
    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
pexpect = [
    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
]
pickleshare = [
    {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
    {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
pillow = [
    {file = "Pillow-9.0.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:113723312215b25c22df1fdf0e2da7a3b9c357a7d24a93ebbe80bfda4f37a8d4"},
    {file = "Pillow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb47a548cea95b86494a26c89d153fd31122ed65255db5dcbc421a2d28eb3379"},
    {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31b265496e603985fad54d52d11970383e317d11e18e856971bdbb86af7242a4"},
    {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d154ed971a4cc04b93a6d5b47f37948d1f621f25de3e8fa0c26b2d44f24e3e8f"},
    {file = "Pillow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fe92813d208ce8aa7d76da878bdc84b90809f79ccbad2a288e9bcbeac1d9bd"},
    {file = "Pillow-9.0.0-cp310-cp310-win32.whl", hash = "sha256:d5dcea1387331c905405b09cdbfb34611050cc52c865d71f2362f354faee1e9f"},
    {file = "Pillow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:52abae4c96b5da630a8b4247de5428f593465291e5b239f3f843a911a3cf0105"},
    {file = "Pillow-9.0.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:72c3110228944019e5f27232296c5923398496b28be42535e3b2dc7297b6e8b6"},
    {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b6d21771da41497b81652d44191489296555b761684f82b7b544c49989110f"},
    {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72f649d93d4cc4d8cf79c91ebc25137c358718ad75f99e99e043325ea7d56100"},
    {file = "Pillow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aaf07085c756f6cb1c692ee0d5a86c531703b6e8c9cae581b31b562c16b98ce"},
    {file = "Pillow-9.0.0-cp37-cp37m-win32.whl", hash = "sha256:03b27b197deb4ee400ed57d8d4e572d2d8d80f825b6634daf6e2c18c3c6ccfa6"},
    {file = "Pillow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a09a9d4ec2b7887f7a088bbaacfd5c07160e746e3d47ec5e8050ae3b2a229e9f"},
    {file = "Pillow-9.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:490e52e99224858f154975db61c060686df8a6b3f0212a678e5d2e2ce24675c9"},
    {file = "Pillow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:500d397ddf4bbf2ca42e198399ac13e7841956c72645513e8ddf243b31ad2128"},
    {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ebd8b9137630a7bbbff8c4b31e774ff05bbb90f7911d93ea2c9371e41039b52"},
    {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd0e5062f11cb3e730450a7d9f323f4051b532781026395c4323b8ad055523c4"},
    {file = "Pillow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f3b4522148586d35e78313db4db0df4b759ddd7649ef70002b6c3767d0fdeb7"},
    {file = "Pillow-9.0.0-cp38-cp38-win32.whl", hash = "sha256:0b281fcadbb688607ea6ece7649c5d59d4bbd574e90db6cd030e9e85bde9fecc"},
    {file = "Pillow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5050d681bcf5c9f2570b93bee5d3ec8ae4cf23158812f91ed57f7126df91762"},
    {file = "Pillow-9.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c2067b3bb0781f14059b112c9da5a91c80a600a97915b4f48b37f197895dd925"},
    {file = "Pillow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d16b6196fb7a54aff6b5e3ecd00f7c0bab1b56eee39214b2b223a9d938c50af"},
    {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98cb63ca63cb61f594511c06218ab4394bf80388b3d66cd61d0b1f63ee0ea69f"},
    {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc462d24500ba707e9cbdef436c16e5c8cbf29908278af053008d9f689f56dee"},
    {file = "Pillow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3586e12d874ce2f1bc875a3ffba98732ebb12e18fb6d97be482bd62b56803281"},
    {file = "Pillow-9.0.0-cp39-cp39-win32.whl", hash = "sha256:68e06f8b2248f6dc8b899c3e7ecf02c9f413aab622f4d6190df53a78b93d97a5"},
    {file = "Pillow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:6579f9ba84a3d4f1807c4aab4be06f373017fc65fff43498885ac50a9b47a553"},
    {file = "Pillow-9.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:47f5cf60bcb9fbc46011f75c9b45a8b5ad077ca352a78185bd3e7f1d294b98bb"},
    {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fd8053e1f8ff1844419842fd474fc359676b2e2a2b66b11cc59f4fa0a301315"},
    {file = "Pillow-9.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c5439bfb35a89cac50e81c751317faea647b9a3ec11c039900cd6915831064d"},
    {file = "Pillow-9.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95545137fc56ce8c10de646074d242001a112a92de169986abd8c88c27566a05"},
    {file = "Pillow-9.0.0.tar.gz", hash = "sha256:ee6e2963e92762923956fe5d3479b1fdc3b76c83f290aad131a2f98c3df0593e"},
]
platformdirs = [
    {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
    {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
]
pluggy = [
    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
prompt-toolkit = [
    {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"},
    {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"},
]
psutil = [
    {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"},
    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"},
    {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"},
    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"},
    {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"},
    {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"},
    {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"},
    {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"},
    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"},
    {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"},
    {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"},
    {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"},
    {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"},
    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"},
    {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"},
    {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"},
    {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"},
    {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"},
    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"},
    {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"},
    {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"},
    {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"},
    {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"},
    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"},
    {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"},
    {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"},
    {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"},
    {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"},
]
ptyprocess = [
    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
py = [
    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pycparser = [
    {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
    {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pyflakes = [
    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
pygments = [
    {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
    {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
]
pymdown-extensions = [
    {file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
    {file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"},
]
pyparsing = [
    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pyperclip = [
    {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"},
]
pytest = [
    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
]
pytest-asyncio = [
    {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"},
    {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"},
]
pytest-cov = [
    {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
    {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
]
pytest-describe = [
    {file = "pytest-describe-2.0.0.tar.gz", hash = "sha256:71b80ccf55e7d18c682e5048516a9f5b0aca0e93f31822424a62c87aafc96ef6"},
    {file = "pytest_describe-2.0.0-py3-none-any.whl", hash = "sha256:4f179634074afbf9a5305b33e46ecb5263d21b212c344a49f618eb6f1278c143"},
]
pytest-expecter = [
    {file = "pytest-expecter-2.3.tar.gz", hash = "sha256:8e6a3e565fbc524e5a4988b664d1b748e0a810b33233880aa5d3d78970351e06"},
    {file = "pytest_expecter-2.3-py3-none-any.whl", hash = "sha256:6336d7f43221500e392014a949fd77ee2c2a84bcae2be7669acdd0d7c89b89e9"},
]
pytest-random = [
    {file = "pytest-random-0.02.tar.gz", hash = "sha256:92f25db8c5d9ffc20d90b51997b914372d6955cb9cf1f6ead45b90514fc0eddd"},
]
pytest-rerunfailures = [
    {file = "pytest-rerunfailures-9.1.1.tar.gz", hash = "sha256:1cb11a17fc121b3918414eb5eaf314ee325f2e693ac7cb3f6abf7560790827f2"},
    {file = "pytest_rerunfailures-9.1.1-py3-none-any.whl", hash = "sha256:2eb7d0ad651761fbe80e064b0fd415cf6730cdbc53c16a145fd84b66143e609f"},
]
python-dateutil = [
    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
pywin32 = [
    {file = "pywin32-301-cp35-cp35m-win32.whl", hash = "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7"},
    {file = "pywin32-301-cp35-cp35m-win_amd64.whl", hash = "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72"},
    {file = "pywin32-301-cp36-cp36m-win32.whl", hash = "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0"},
    {file = "pywin32-301-cp36-cp36m-win_amd64.whl", hash = "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78"},
    {file = "pywin32-301-cp37-cp37m-win32.whl", hash = "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b"},
    {file = "pywin32-301-cp37-cp37m-win_amd64.whl", hash = "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a"},
    {file = "pywin32-301-cp38-cp38-win32.whl", hash = "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17"},
    {file = "pywin32-301-cp38-cp38-win_amd64.whl", hash = "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96"},
    {file = "pywin32-301-cp39-cp39-win32.whl", hash = "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe"},
    {file = "pywin32-301-cp39-cp39-win_amd64.whl", hash = "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf"},
]
pyyaml = [
    {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
    {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
    {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
    {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
    {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
    {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
    {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
    {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
    {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
    {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
    {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
    {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
    {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
    {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
    {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
    {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
    {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
    {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
    {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
]
pyyaml-env-tag = [
    {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
    {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
pyzmq = [
    {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd"},
    {file = "pyzmq-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149"},
    {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"},
    {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"},
    {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"},
    {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"},
    {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"},
    {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"},
    {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"},
    {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"},
    {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"},
    {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"},
    {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"},
    {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"},
    {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"},
    {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"},
    {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"},
    {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"},
    {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"},
    {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"},
    {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"},
    {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"},
    {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"},
    {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"},
    {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"},
    {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"},
    {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"},
    {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"},
    {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"},
    {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"},
    {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"},
    {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"},
    {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"},
    {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"},
    {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973"},
    {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"},
    {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"},
    {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"},
    {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"},
    {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"},
    {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"},
    {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"},
    {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"},
    {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf"},
    {file = "pyzmq-22.3.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb"},
    {file = "pyzmq-22.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115"},
    {file = "pyzmq-22.3.0.tar.gz", hash = "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c"},
]
requests = [
    {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
    {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
rfc3986 = [
    {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
    {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
rope = [
    {file = "rope-0.18.0.tar.gz", hash = "sha256:786b5c38c530d4846aa68a42604f61b4e69a493390e3ca11b88df0fbfdc3ed04"},
]
roundrobin = [
    {file = "roundrobin-0.0.2.tar.gz", hash = "sha256:ac30cb78570a36bb0ce0db7b907af9394ec7a5610ece2ede072280e8dd867caa"},
]
"ruamel.yaml" = [
    {file = "ruamel.yaml-0.17.20-py3-none-any.whl", hash = "sha256:810eef9c46523a3f77479c66267a4708255ebe806a2d540078408c2227f011af"},
    {file = "ruamel.yaml-0.17.20.tar.gz", hash = "sha256:4b8a33c1efb2b443a93fcaafcfa4d2e445f8e8c29c528d9f5cdafb7cc9e4004c"},
]
"ruamel.yaml.clib" = [
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"},
    {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
    {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
    {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
    {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
    {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
    {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
    {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
]
sanic = [
    {file = "sanic-21.12.1-py3-none-any.whl", hash = "sha256:53230ba1a1081e6b075e58339a47c07d2bf618119fa40ba88c69fe57c99126f1"},
    {file = "sanic-21.12.1.tar.gz", hash = "sha256:6f15ecfef47d4288aac04d8741833a7701cd94f2d4a28fc5667ce2844363152d"},
]
sanic-ext = [
    {file = "sanic-ext-22.1.2.tar.gz", hash = "sha256:4645014137115706a21b9ef0c8f05f4d6332769dc419ee65563b70e0b4fcc86e"},
    {file = "sanic_ext-22.1.2-py3-none-any.whl", hash = "sha256:880740b5961fcfbbe8caca735c4b93691f0fdf5e2f779480228548bddb110c98"},
]
sanic-routing = [
    {file = "sanic-routing-0.7.1.tar.gz", hash = "sha256:35b09a12aceecc4e295bddb0ed39cff90363dd1504f9831c6b0b01f6f026635d"},
    {file = "sanic_routing-0.7.1-py3-none-any.whl", hash = "sha256:0c08f5f65a55fdca259d9149d89c39fd2377a651b408065fd643bdabdb6ba293"},
]
sanic-testing = [
    {file = "sanic-testing-0.8.2.tar.gz", hash = "sha256:dd7123132e159281b14eb6434da811e2082165432aa2c523262e44b2c09c3be0"},
    {file = "sanic_testing-0.8.2-py3-none-any.whl", hash = "sha256:f2c3679cd498351f095d8687a1cc6cc10558fd69e014d060ec21f3a020d5723b"},
]
six = [
    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
sniffio = [
    {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
    {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
]
spongemock = [
    {file = "spongemock-0.3.4-py2.py3-none-any.whl", hash = "sha256:6a7df0a929a8d0f55cae1b2cfb45db471775f6fda0f2c2239d9935dbe6ac500b"},
    {file = "spongemock-0.3.4.tar.gz", hash = "sha256:40c1316d9ece635d727ac2b3c6e22e5f9d85ba402ee4dc6eeef5a8e8c17ff892"},
]
toml = [
    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tomli = [
    {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
    {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
]
tomlkit = [
    {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"},
    {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"},
]
traitlets = [
    {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"},
    {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"},
]
types-aiofiles = [
    {file = "types-aiofiles-0.1.9.tar.gz", hash = "sha256:3ccfe55e17a97143c7a3730ed52b24385171212d4bc76c63c867c437512ef55e"},
    {file = "types_aiofiles-0.1.9-py3-none-any.whl", hash = "sha256:36b806484b4ce1d29327157ba46e4c901cddef522cce6516d229c1f5b54c1ff0"},
]
types-pkg-resources = [
    {file = "types-pkg_resources-0.1.3.tar.gz", hash = "sha256:834a9b8d3dbea343562fd99d5d3359a726f6bf9d3733bccd2b4f3096fbab9dae"},
    {file = "types_pkg_resources-0.1.3-py2.py3-none-any.whl", hash = "sha256:0cb9972cee992249f93fff1a491bf2dc3ce674e5a1926e27d4f0866f7d9b6d9c"},
]
types-requests = [
    {file = "types-requests-2.25.9.tar.gz", hash = "sha256:4ec8b71da73e5344adb9bee725a74ec8598e7286f9bcb17500d627f259fe4fb9"},
    {file = "types_requests-2.25.9-py3-none-any.whl", hash = "sha256:543ba8b3b23e38ac028da1d163aecbbc27d3cc8f654ae64339da539a191a2b1c"},
]
typing-extensions = [
    {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
    {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
    {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
ujson = [
    {file = "ujson-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:644552d1e89983c08d0c24358fbcb5829ae5b5deee9d876e16d20085cfa7dc81"},
    {file = "ujson-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0cae4a9c141856f7ad1a79c17ff1aaebf7fd8faa2f2c2614c37d6f82ed261d96"},
    {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba63b789d83ca92237dbc72041a268d91559f981c01763a107105878bae442e"},
    {file = "ujson-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4e8f71e2fd42dce245bace7e2aa97dabef13926750a351eadca89a1e0f1abd"},
    {file = "ujson-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f73946c047a38640b1f5a2a459237b7bdc417ab028a76c796e4eea984b359b9"},
    {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:afe91153c2046fa8210b92def513124e0ea5b87ad8fa4c14fef8197204b980f1"},
    {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b1ef400fc73ab0cb61b74a662ad4207917223aba6f933a9fea9b0fbe75de2361"},
    {file = "ujson-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c8a884d60dd2eed2fc95a9474d57ead82adf254f54caffb3d9e8ed185c49aba"},
    {file = "ujson-5.1.0-cp310-cp310-win32.whl", hash = "sha256:173b90a2c2836ee42f708df88ecfe3efbc4d868df73c9fcea8cb8f6f3ab93892"},
    {file = "ujson-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c45ad95e82155372d9908774db46e0ef7880af28a734d0b14eaa4f505e64982"},
    {file = "ujson-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4155a7c29bf330329519027c815e15e381c1fff22f50d26f135584d482bbd95d"},
    {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa616d0d3c594785c6e9b7f42686bb1c86f9e64aa0f30a72c86d8eb315f54194"},
    {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a48efcb5d3695b295c26835ed81048da8cd40e76c4fde2940c807aa452b560c9"},
    {file = "ujson-5.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838d35eb9006d36f9241e95958d9f4819bcf1ea2ec155daf92d5751c31bcc62b"},
    {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:05aa6c7297a22081f65497b6f586de6b7060ea47c3ecda80896f47200e9dbf04"},
    {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce441ab7ad1db592e2db95b6c2a1eb882123532897340afac1342c28819e9833"},
    {file = "ujson-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9937e819196b894ffd00801b24f1042dabda142f355313c3f20410993219bc4f"},
    {file = "ujson-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:06bed66ae62d517f67a61cf53c056800b35ef364270723168a1db62702e2d30c"},
    {file = "ujson-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:74e41a0222e6e8136e38f103d6cc228e4e20f1c35cc80224a42804fd67fb35c8"},
    {file = "ujson-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7bbb87f040e618bebe8c6257b3e4e8ae2f708dcbff3270c84718b3360a152799"},
    {file = "ujson-5.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68e38122115a8097fbe1cfe52979a797eaff91c10c1bf4b27774e5f30e7f723a"},
    {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b09843123425337d2efee5c8ff6519e4dfc7b044db66c8bd560517fc1070a157"},
    {file = "ujson-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dca10174a3bd482d969a2d12d0aec2fdd63fb974e255ec0147e36a516a2d68a"},
    {file = "ujson-5.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202ae52f4a53f03c42ead6d046b1a146517e93bd757f517bdeef0a26228e0260"},
    {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7a4bed7bd7b288cf73ba47bda27fdd1d78ef6906831489e7f296aef9e786eccb"},
    {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d423956f8dfd98a075c9338b886414b6e3c2817dbf67935797466c998af39936"},
    {file = "ujson-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:083c1078e4de3a39019e590c43865b17e07a763fee25b012e650bb4f42c89703"},
    {file = "ujson-5.1.0-cp38-cp38-win32.whl", hash = "sha256:31671ad99f0395eb881d698f2871dc64ff00fbd4380c5d9bfd8bff3d4c8f8d88"},
    {file = "ujson-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:994eaf4369e6bc24258f59fe8c6345037abcf24557571814e27879851c4353aa"},
    {file = "ujson-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00d6ea9702c2eaeaf1a826934eaba1b4c609c873379bf54e36ba7b7e128edf94"},
    {file = "ujson-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a53c4fe8e1c067e6c98b4526e982ed9486f08578ad8eb5f0e94f8cadf0c1d911"},
    {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:368f855779fded560724a6448838304621f498113a116d66bc5ed5ad5ad3ca92"},
    {file = "ujson-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd97e45a0f450ba2c43cda18147e54b8e41e886c22e3506c62f7d61e9e53b0d"},
    {file = "ujson-5.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caeadbf95ce277f1f8f4f71913bc20c01f49fc9228f238920f9ff6f7645d2a5f"},
    {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:681fed63c948f757466eeb3aea98873e2ab8b2b18e9020c96a97479a513e2018"},
    {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6fc4376266ae67f6d8f9e69386ab950eb84ba345c6fdbeb1884fa5b773c8c76b"},
    {file = "ujson-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:585271d6ad545a2ccfc237582f70c160e627735c89d0ca2bde24afa321bc0750"},
    {file = "ujson-5.1.0-cp39-cp39-win32.whl", hash = "sha256:b631af423e6d5d35f9f37fbcc4fbdb6085abc1c441cf864c64b7fbb5b150faf7"},
    {file = "ujson-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:08265db5ccff8b521ff68aee13a417d68cca784d7e711d961b92fda6ccffcc4f"},
    {file = "ujson-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e2b1c372583eb4363b42e21222d3a18116a41973781d502d61e1b0daf4b8352f"},
    {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51142c9d40439f299594e399bef8892a16586ded54c88d3af926865ca221a177"},
    {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ba8be1717b1867a85b2413a8585bad0e4507a22d6af2c244e1c74151f6d5cc0"},
    {file = "ujson-5.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b26d9d6eb9a0979d37f28c715e717a409c9e03163e5cd8fa73aab806351ab5"},
    {file = "ujson-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b2c7e4afde0d36926b091fa9613b18b65e911fcaa60024e8721f2dcfedc25329"},
    {file = "ujson-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:110633a8dda6c8ca78090292231e15381f8b2423e998399d4bc5f135149c722b"},
    {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdac161127ef8e0889180a4c07475457c55fe0bbd644436d8f4c7ef07565d653"},
    {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:452990c2b18445a7379a45873527d2ec47789b9289c13a17a3c1cc76b9641126"},
    {file = "ujson-5.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5304ad25d100d50b5bc8513ef110335df678f66c7ccf3d4728c0c3aa69e08e0c"},
    {file = "ujson-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ce620a6563b21aa3fbb1658bc1bfddb484a6dad542de1efb5121eb7bb4f2b93a"},
    {file = "ujson-5.1.0.tar.gz", hash = "sha256:a88944d2f99db71a3ca0c63d81f37e55b660edde0b07216fb65a3e46403ef004"},
]
urllib3 = [
    {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
    {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
uvicorn = [
    {file = "uvicorn-0.14.0-py3-none-any.whl", hash = "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae"},
    {file = "uvicorn-0.14.0.tar.gz", hash = "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292"},
]
uvloop = [
    {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
    {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
    {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"},
    {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"},
    {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"},
    {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"},
    {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"},
    {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"},
    {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"},
    {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"},
    {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"},
    {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"},
    {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"},
    {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"},
    {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
    {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
]
watchdog = [
    {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"},
    {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"},
    {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"},
    {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"},
    {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"},
    {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"},
    {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"},
    {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"},
    {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"},
    {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"},
    {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"},
    {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"},
    {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"},
    {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"},
    {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"},
    {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"},
    {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"},
]
wcwidth = [
    {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
    {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
webob = [
    {file = "WebOb-1.8.7-py2.py3-none-any.whl", hash = "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b"},
    {file = "WebOb-1.8.7.tar.gz", hash = "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"},
]
websockets = [
    {file = "websockets-10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da"},
    {file = "websockets-10.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939"},
    {file = "websockets-10.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8"},
    {file = "websockets-10.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379"},
    {file = "websockets-10.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7"},
    {file = "websockets-10.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c"},
    {file = "websockets-10.0-cp37-cp37m-win32.whl", hash = "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76"},
    {file = "websockets-10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80"},
    {file = "websockets-10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b"},
    {file = "websockets-10.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537"},
    {file = "websockets-10.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456"},
    {file = "websockets-10.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4"},
    {file = "websockets-10.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238"},
    {file = "websockets-10.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539"},
    {file = "websockets-10.0-cp38-cp38-win32.whl", hash = "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465"},
    {file = "websockets-10.0-cp38-cp38-win_amd64.whl", hash = "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a"},
    {file = "websockets-10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805"},
    {file = "websockets-10.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37"},
    {file = "websockets-10.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a"},
    {file = "websockets-10.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1"},
    {file = "websockets-10.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2"},
    {file = "websockets-10.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474"},
    {file = "websockets-10.0-cp39-cp39-win32.whl", hash = "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368"},
    {file = "websockets-10.0-cp39-cp39-win_amd64.whl", hash = "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567"},
    {file = "websockets-10.0.tar.gz", hash = "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002"},
]
werkzeug = [
    {file = "Werkzeug-2.0.2-py3-none-any.whl", hash = "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f"},
    {file = "Werkzeug-2.0.2.tar.gz", hash = "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a"},
]
yarl = [
    {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e35d8230e4b08d86ea65c32450533b906a8267a87b873f2954adeaecede85169"},
    {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb4b3f277880c314e47720b4b6bb2c85114ab3c04c5442c9bc7006b3787904d8"},
    {file = "yarl-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7015dcedb91d90a138eebdc7e432aec8966e0147ab2a55f2df27b1904fa7291"},
    {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3e478175e15e00d659fb0354a6a8db71a7811a2a5052aed98048bc972e5d2b"},
    {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8c409aa3a7966647e7c1c524846b362a6bcbbe120bf8a176431f940d2b9a2e"},
    {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b22ea41c7e98170474a01e3eded1377d46b2dfaef45888a0005c683eaaa49285"},
    {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a7dfc46add4cfe5578013dbc4127893edc69fe19132d2836ff2f6e49edc5ecd6"},
    {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82ff6f85f67500a4f74885d81659cd270eb24dfe692fe44e622b8a2fd57e7279"},
    {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f3cd2158b2ed0fb25c6811adfdcc47224efe075f2d68a750071dacc03a7a66e4"},
    {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:59c0f13f9592820c51280d1cf811294d753e4a18baf90f0139d1dc93d4b6fc5f"},
    {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f7655ad83d1a8afa48435a449bf2f3009293da1604f5dd95b5ddcf5f673bd69"},
    {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aa9f0d9b62d15182341b3e9816582f46182cab91c1a57b2d308b9a3c4e2c4f78"},
    {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fdd1b90c225a653b1bd1c0cae8edf1957892b9a09c8bf7ee6321eeb8208eac0f"},
    {file = "yarl-1.7.0-cp310-cp310-win32.whl", hash = "sha256:7c8d0bb76eabc5299db203e952ec55f8f4c53f08e0df4285aac8c92bd9e12675"},
    {file = "yarl-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:622a36fa779efb4ff9eff5fe52730ff17521431379851a31e040958fc251670c"},
    {file = "yarl-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d461b7a8e139b9e4b41f62eb417ffa0b98d1c46d4caf14c845e6a3b349c0bb1"},
    {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81cfacdd1e40bc931b5519499342efa388d24d262c30a3d31187bfa04f4a7001"},
    {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:821b978f2152be7695d4331ef0621d207aedf9bbd591ba23a63412a3efc29a01"},
    {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b64bd24c8c9a487f4a12260dc26732bf41028816dbf0c458f17864fbebdb3131"},
    {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98c9ddb92b60a83c21be42c776d3d9d5ec632a762a094c41bda37b7dfbd2cd83"},
    {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a532d75ca74431c053a88a802e161fb3d651b8bf5821a3440bc3616e38754583"},
    {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:053e09817eafb892e94e172d05406c1b3a22a93bc68f6eff5198363a3d764459"},
    {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:98c51f02d542945d306c8e934aa2c1e66ba5e9c1c86b5bf37f3a51c8a747067e"},
    {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:15ec41a5a5fdb7bace6d7b16701f9440007a82734f69127c0fbf6d87e10f4a1e"},
    {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a7f08819dba1e1255d6991ed37448a1bf4b1352c004bcd899b9da0c47958513d"},
    {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8e3ffab21db0542ffd1887f3b9575ddd58961f2cf61429cb6458afc00c4581e0"},
    {file = "yarl-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:50127634f519b2956005891507e3aa4ac345f66a7ea7bbc2d7dcba7401f41898"},
    {file = "yarl-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:36ec44f15193f6d5288d42ebb8e751b967ebdfb72d6830983838d45ab18edb4f"},
    {file = "yarl-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ec1b5a25a25c880c976d0bb3d107def085bb08dbb3db7f4442e0a2b980359d24"},
    {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b36f5a63c891f813c6f04ef19675b382efc190fd5ce7e10ab19386d2548bca06"},
    {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38173b8c3a29945e7ecade9a3f6ff39581eee8201338ee6a2c8882db5df3e806"},
    {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba402f32184f0b405fb281b93bd0d8ab7e3257735b57b62a6ed2e94cdf4fe50"},
    {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:be52bc5208d767cdd8308a9e93059b3b36d1e048fecbea0e0346d0d24a76adc0"},
    {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08c2044a956f4ef30405f2f433ce77f1f57c2c773bf81ae43201917831044d5a"},
    {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:484d61c047c45670ef5967653a1d0783e232c54bf9dd786a7737036828fa8d54"},
    {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b7de92a4af85cfcaf4081f8aa6165b1d63ee5de150af3ee85f954145f93105a7"},
    {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:376e41775aab79c5575534924a386c8e0f1a5d91db69fc6133fd27a489bcaf10"},
    {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:8a8b10d0e7bac154f959b709fcea593cda527b234119311eb950096653816a86"},
    {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f46cd4c43e6175030e2a56def8f1d83b64e6706eeb2bb9ab0ef4756f65eab23f"},
    {file = "yarl-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:b28cfb46140efe1a6092b8c5c4994a1fe70dc83c38fbcea4992401e0c6fb9cce"},
    {file = "yarl-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9624154ec9c02a776802da1086eed7f5034bd1971977f5146233869c2ac80297"},
    {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:69945d13e1bbf81784a9bc48824feb9cd66491e6a503d4e83f6cd7c7cc861361"},
    {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:46a742ed9e363bd01be64160ce7520e92e11989bd4cb224403cfd31c101cc83d"},
    {file = "yarl-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb4ff1ac7cb4500f43581b3f4cbd627d702143aa6be1fdc1fa3ebffaf4dc1be5"},
    {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ad51e17cd65ea3debb0e10f0120cf8dd987c741fe423ed2285087368090b33d"},
    {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e37786ea89a5d3ffbbf318ea9790926f8dfda83858544f128553c347ad143c6"},
    {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c63c1e208f800daad71715786bfeb1cecdc595d87e2e9b1cd234fd6e597fd71d"},
    {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91cbe24300c11835ef186436363352b3257db7af165e0a767f4f17aa25761388"},
    {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e510dbec7c59d32eaa61ffa48173d5e3d7170a67f4a03e8f5e2e9e3971aca622"},
    {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3def6e681cc02397e5d8141ee97b41d02932b2bcf0fb34532ad62855eab7c60e"},
    {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:263c81b94e6431942b27f6f671fa62f430a0a5c14bb255f2ab69eeb9b2b66ff7"},
    {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e78c91faefe88d601ddd16e3882918dbde20577a2438e2320f8239c8b7507b8f"},
    {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:22b2430c49713bfb2f0a0dd4a8d7aab218b28476ba86fd1c78ad8899462cbcf2"},
    {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e7ad9db939082f5d0b9269cfd92c025cb8f2fbbb1f1b9dc5a393c639db5bd92"},
    {file = "yarl-1.7.0-cp38-cp38-win32.whl", hash = "sha256:3a31e4a8dcb1beaf167b7e7af61b88cb961b220db8d3ba1c839723630e57eef7"},
    {file = "yarl-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d579957439933d752358c6a300c93110f84aae67b63dd0c19dde6ecbf4056f6b"},
    {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:87721b549505a546eb003252185103b5ec8147de6d3ad3714d148a5a67b6fe53"},
    {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1fa866fa24d9f4108f9e58ea8a2135655419885cdb443e36b39a346e1181532"},
    {file = "yarl-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d3b8449dfedfe94eaff2b77954258b09b24949f6818dfa444b05dbb05ae1b7e"},
    {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2372e350794ce8b9f810feb094c606b7e0e4aa6807141ac4fadfe5ddd75bb0"},
    {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a06d9d0b9a97fa99b84fee71d9dd11e69e21ac8a27229089f07b5e5e50e8d63c"},
    {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3455c2456d6307bcfa80bc1157b8603f7d93573291f5bdc7144489ca0df4628"},
    {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d30d67e3486aea61bb2cbf7cf81385364c2e4f7ce7469a76ed72af76a5cdfe6b"},
    {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c18a4b286e8d780c3a40c31d7b79836aa93b720f71d5743f20c08b7e049ca073"},
    {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d54c925396e7891666cabc0199366ca55b27d003393465acef63fd29b8b7aa92"},
    {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:64773840952de17851a1c7346ad7f71688c77e74248d1f0bc230e96680f84028"},
    {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:acbf1756d9dc7cd0ae943d883be72e84e04396f6c2ff93a6ddeca929d562039f"},
    {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2e48f27936aa838939c798f466c851ba4ae79e347e8dfce43b009c64b930df12"},
    {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1beef4734ca1ad40a9d8c6b20a76ab46e3a2ed09f38561f01e4aa2ea82cafcef"},
    {file = "yarl-1.7.0-cp39-cp39-win32.whl", hash = "sha256:8ee78c9a5f3c642219d4607680a4693b59239c27a3aa608b64ef79ddc9698039"},
    {file = "yarl-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:d750503682605088a14d29a4701548c15c510da4f13c8b17409c4097d5b04c52"},
    {file = "yarl-1.7.0.tar.gz", hash = "sha256:8e7ebaf62e19c2feb097ffb7c94deb0f0c9fab52590784c8cd679d30ab009162"},
]
zipp = [
    {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
    {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
]
"zope.event" = [
    {file = "zope.event-4.5.0-py2.py3-none-any.whl", hash = "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42"},
    {file = "zope.event-4.5.0.tar.gz", hash = "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330"},
]
"zope.interface" = [
    {file = "zope.interface-5.4.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7"},
    {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021"},
    {file = "zope.interface-5.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192"},
    {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a"},
    {file = "zope.interface-5.4.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531"},
    {file = "zope.interface-5.4.0-cp27-cp27m-win32.whl", hash = "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325"},
    {file = "zope.interface-5.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155"},
    {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"},
    {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959"},
    {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e"},
    {file = "zope.interface-5.4.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c"},
    {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702"},
    {file = "zope.interface-5.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f"},
    {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05"},
    {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004"},
    {file = "zope.interface-5.4.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117"},
    {file = "zope.interface-5.4.0-cp35-cp35m-win32.whl", hash = "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8"},
    {file = "zope.interface-5.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63"},
    {file = "zope.interface-5.4.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f"},
    {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920"},
    {file = "zope.interface-5.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46"},
    {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc"},
    {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9"},
    {file = "zope.interface-5.4.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2"},
    {file = "zope.interface-5.4.0-cp36-cp36m-win32.whl", hash = "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78"},
    {file = "zope.interface-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1"},
    {file = "zope.interface-5.4.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e"},
    {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b"},
    {file = "zope.interface-5.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f"},
    {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d"},
    {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8"},
    {file = "zope.interface-5.4.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf"},
    {file = "zope.interface-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7"},
    {file = "zope.interface-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94"},
    {file = "zope.interface-5.4.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3"},
    {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e"},
    {file = "zope.interface-5.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7"},
    {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120"},
    {file = "zope.interface-5.4.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48"},
    {file = "zope.interface-5.4.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4"},
    {file = "zope.interface-5.4.0-cp38-cp38-win32.whl", hash = "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb"},
    {file = "zope.interface-5.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54"},
    {file = "zope.interface-5.4.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4"},
    {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d"},
    {file = "zope.interface-5.4.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83"},
    {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25"},
    {file = "zope.interface-5.4.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1"},
    {file = "zope.interface-5.4.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c"},
    {file = "zope.interface-5.4.0-cp39-cp39-win32.whl", hash = "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e"},
    {file = "zope.interface-5.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09"},
    {file = "zope.interface-5.4.0.tar.gz", hash = "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e"},
]

mkdocs.yml METASEP
site_name: Memegen.link
site_description: The free and open source API to generate memes.
site_author: Jace Browning

repo_url: https://github.com/jacebrowning/memegen
edit_uri: ""

nav:
  - Home: index.md
  - Clients: clients.md
  - Examples: examples.md

theme:
  name: mkdocs
  custom_dir: docs/theme
  nav_style: dark
  analytics:
    gtag: G-B2XT39Z5FC

plugins: []

remote_branch: site

app.json METASEP
{
  "addons": [],
  "buildpacks": [
    {
      "url": "heroku/python"
    }
  ],
  "env": {
    "HEROKU_APP_NAME": {
      "required": true
    },
    "MAX_REQUESTS": {
      "requied": true
    }
  },
  "formation": {
    "web": {
      "quantity": 1
    }
  },
  "name": "memegen",
  "scripts": {},
  "stack": "heroku-18"
}

README.md METASEP
An API to programmatically generate memes based solely on requested URLs.

<span class="badges"><!-- badges -->
[![Build Status](https://img.shields.io/circleci/build/github/jacebrowning/memegen)](https://circleci.com/gh/jacebrowning/memegen)
[![Coverage Status](http://img.shields.io/coveralls/jacebrowning/memegen/main.svg)](https://coveralls.io/r/jacebrowning/memegen)
[![Swagger Validator](https://img.shields.io/swagger/valid/3.0?label=docs&specUrl=https%3A%2F%2Fapi.memegen.link%2Fdocs%2Fswagger.json)](https://api.memegen.link/docs/)
[![License](https://img.shields.io/badge/license-mit-blue)](https://github.com/jacebrowning/memegen/blob/main/LICENSE.txt)
[![GitHub Sponsors](https://img.shields.io/endpoint?url=https://memecomplete.com/api/memes/badge.json)](https://github.com/sponsors/jacebrowning)
</span>

[Buy me a coffee to help keep this site running!](https://www.buymeacoffee.com/jacebrowning)

---

# Images

The API is stateless so URLs contain all the information necessary to generate meme images. For example, <https://api.memegen.link/images/buzz/memes/memes_everywhere.gif> produces:

![Example Image](https://api.memegen.link/images/buzz/memes/memes_everywhere.gif?token=icc12mvkq7hc9ysbbtxr&width=800&frames=50)

## Available Formats

Clients can request `.jpg` instead of `.png` for smaller files or `.gif` if an animated background is available:

| Format | Example                                                                                                  |
| :----- | :------------------------------------------------------------------------------------------------------- |
| PNG    | [/images/ds/small_file/high_quality.png](https://api.memegen.link/images/ds/small_file/high_quality.png) |
| JPEG   | [/images/ds/high_quality/small_file.jpg](https://api.memegen.link/images/ds/high_quality/small_file.jpg) |
| GIF    | [/images/oprah/you_get/animated_text.gif](https://api.memegen.link/oprah/you_get/animated_text.gif)      |

## Custom Dimensions

Images can be scaled to a specific width or height using the `width=<int>` and `height=<int>` query parameters. If both are provided (`width=<int>&height=<int>`), the image will be padded to the exact dimensions.

For example, <https://api.memegen.link/images/both/width_or_height/why_not_both~q.png?height=450&width=800> produces:

![Custom Size](https://api.memegen.link/images/both/width_or_height/why_not_both~q.png?height=450&width=800&token=e3ctlu471cv4k0hx698p)

## Special Characters

In URLs, spaces can be inserted using underscores or dashes:

- underscore (`_`) → space (` `)
- dash (`-`) → space (` `)
- 2 underscores (`__`) → underscore (`_`)
- 2 dashes (`--`) → dash (`-`)
- tilde + N (`~n`) → newline character

Reserved URL characters can be included using escape patterns:

- tilde + Q (`~q`) → question mark (`?`)
- tilde + A (`~a`) → ampersand (`&`)
- tilde + P (`~p`) → percentage (`%`)
- tilde + H (`~h`) → hashtag/pound (`#`)
- tilde + S (`~s`) → slash (`/`)
- tilde + B (`~b`) → backslash (`\`)
- tilde + L (`~l`) → less-than sign (`<`)
- tilde + G (`~g`) → greater-than sign (`>`)
- 2 single quotes (`''`) → double quote (`"`)

For example, <https://api.memegen.link/images/ugandanknuck/~hspecial_characters~q/underscore__-dash--.png> produces:

![Escaped Characters](https://api.memegen.link/images/ugandanknuck/~hspecial_characters~q/underscore___dash--.png?width=800&token=imyyyyk8qex6m2jz8bar)

All of the `POST` endpoints will return image URLs with special characters replaced with these alternatives.

# Templates

The list of predefined meme templates is available here: <https://api.memegen.link/templates/>

## Alternate Styles

Some memes come in multiple forms, which can be selected using the `style=<str>` query parameter.

For example, the <https://api.memegen.link/templates/ds/> template provides these styles:

|                          `/images/ds.png`                          |                           `/images/ds.png?style=maga`                           |
| :----------------------------------------------------------------: | :-----------------------------------------------------------------------------: |
| ![Default Style](https://api.memegen.link/images/ds.png?width=375) | ![Alternate Style](https://api.memegen.link/images/ds.png?width=375&style=maga) |

## Custom Overlays

The `style=<str>` query parameter can also be an image URL to overlay on the default background image.

For example, <https://api.memegen.link/images/pigeon/Engineer/_/Is_this_Photoshop~q.png?style=https://i.imgur.com/W0NXFpQ.png> produces:

![Custom Overlay](https://api.memegen.link/images/pigeon/Engineer/_/Is_this_Photoshop~q.png?style=https://i.imgur.com/W0NXFpQ.png&width=800&token=cy49tv234bu3jzgw587o)

## Custom Backgrounds

You can also use your own image URL as the background.

For example, <https://api.memegen.link/images/custom/_/my_background.png?background=http://www.gstatic.com/webp/gallery/1.png> produces:

![Custom Background](https://api.memegen.link/images/custom/_/my_background.png?background=http://www.gstatic.com/webp/gallery/1.png&width=800&token=ahby9x2nlsbk0gxdmpo5)

# Fonts

The full list of fonts is available here: <https://api.memegen.link/fonts/>

Add the `font=<str>` query parameter to customize the look of your meme:

| Name                                                                   | ID                  | Alias        |
| ---------------------------------------------------------------------- | ------------------- | ------------ |
| [Titillium Web Black](https://fonts.google.com/specimen/Titillium+Web) | `font=titilliumweb` | `font=thick` |
| [Kalam Regular](https://fonts.google.com/specimen/Kalam)               | `font=kalam`        | `font=comic` |
| [Impact](https://www.dafontfree.io/impact-font/)                       | `font=impact`       | -            |
| [Noto Sans Bold](https://fonts.google.com/noto/specimen/Noto+Sans)     | `font=notosans`     | -            |

<br>

Explore the full API here: <https://api.memegen.link/docs/>

Procfile.dev METASEP
api: DEBUG=true PYTHONUNBUFFERED=true python app/main.py
site: mkdocs serve --dev-addr localhost:5001

Procfile METASEP
web: gunicorn app.main:app --bind 0.0.0.0:${PORT:-5000} --worker-class uvicorn.workers.UvicornWorker --max-requests=${MAX_REQUESTS:-0} --max-requests-jitter=${MAX_REQUESTS_JITTER:-0} --timeout=20
release: bin/purge

Makefile METASEP
export PYTHONBREAKPOINT=ipdb.set_trace

.PHONY: all
all: install

.PHONY: ci
ci: format check test

###############################################################################
# System Dependencies

.PHONY: doctor
doctor:
	bin/verchew --exit-code

###############################################################################
# Project Dependencies

BACKEND_DEPENDENCIES := .venv/.flag

.PHONY: install
install: $(BACKEND_DEPENDENCIES)

$(BACKEND_DEPENDENCIES): poetry.lock runtime.txt requirements.txt
	@ poetry config virtualenvs.in-project true
	poetry install
	@ touch $@

ifndef CI
poetry.lock: pyproject.toml
	poetry lock --no-update
	@ touch $@
runtime.txt: .python-version
	echo "python-$(shell cat $<)" > $@
requirements.txt: poetry.lock
	poetry export --format requirements.txt --output $@ --without-hashes
endif

site: install
	poetry run mkdocs build --strict
	sed -i -e 's/http:\/\/localhost:5000/https:\/\/api.memegen.link/g' site/examples/index.html
	echo memegen.link > site/CNAME
ifeq ($(CIRCLE_BRANCH),main)
	@ echo
	git config --global user.name CircleCI
	poetry run mkdocs gh-deploy --dirty
endif

.PHONY: clean
clean:
	rm -rf app/tests/images images site templates/_custom-* templates/*/_*

.PHONY: clean-all
clean-all: clean
	rm -rf *.egg-info .venv

###############################################################################
# Development Tasks

PACKAGES := app scripts

.PHONY: run
run: install
	poetry run honcho start --procfile Procfile.dev

.PHONY: format
format: install
	poetry run autoflake --recursive $(PACKAGES) --in-place --remove-all-unused-imports --ignore-init-module-imports
	poetry run isort $(PACKAGES)
	poetry run black $(PACKAGES)

.PHONY: check
check: install
	poetry run mypy $(PACKAGES)

.PHONY: test
test: install
ifdef CI
	poetry run pytest --verbose --junit-xml=results/junit.xml
else
	@ if test -e .cache/v/cache/lastfailed; then \
		echo "Running failed tests..." && \
		poetry run pytest --last-failed --maxfail=1 --no-cov && \
		echo "Running all tests..." && \
		poetry run pytest --cache-clear; \
	else \
		echo "Running all tests..." && \
		poetry run pytest --new-first --maxfail=1; \
	fi
endif
ifdef SKIP_SLOW
	poetry run coveragespace update unit
else
	poetry run coveragespace update overall
endif

.PHONY: test-fast
test-fast: install
	poetry run pytest -m "not slow" --durations=10

.PHONY: test-slow
test-slow: install
	poetry run pytest -m slow --durations=0 --durations-min=0.05

.PHONY: dev
dev: install
	@ sleep 2 && touch */__init__.py &
	@ poetry run watchmedo shell-command --recursive --pattern="*.py" --command="clear && make test check format SKIP_SLOW=true && echo && echo ✅ && echo" --wait --drop

###############################################################################
# Delivery Tasks

.PHONY: run-production
run-production: install .env
	poetry run heroku local web

.PHONY: promote
promote: install .env .envrc
	@ echo
	SITE=https://staging.memegen.link poetry run pytest scripts/check_deployment.py --verbose --no-cov --reruns=2
	@ echo
	heroku pipelines:promote --app memegen-staging --to memegen-production
	@ echo
	sleep 30
	@ echo
	SITE=https://api.memegen.link poetry run pytest scripts/check_deployment.py --verbose --no-cov --reruns=2

.env:
	echo WEB_CONCURRENCY=2 >> $@
	echo MAX_REQUESTS=0 >> $@
	echo MAX_REQUESTS_JITTER=0 >> $@

.envrc:
	echo dotenv >> $@
	echo >> $@
	echo "export CF_API_KEY=???" >> $@
	echo "export REMOTE_TRACKING_URL=???" >> $@

Containerfile METASEP
ARG ARG_PORT=5000
ARG ARG_MAX_REQUESTS=0
ARG ARG_MAX_REQUESTS_JITTER=0

# Prep everything
FROM docker.io/python:3.10-bullseye as build

# Create the memegen user
RUN useradd -md /opt/memegen -u 1000 memegen
USER memegen

# Set the Working Directory to /opt/memegen
WORKDIR /opt/memegen

# Copy Directories
COPY templates /opt/memegen/templates
COPY scripts /opt/memegen/scripts
COPY fonts /opt/memegen/fonts
COPY docs /opt/memegen/docs
COPY bin /opt/memegen/bin
COPY app /opt/memegen/app

# Copy Specific Files
COPY requirements.txt /opt/memegen
COPY pyproject.toml /opt/memegen/
COPY runtime.txt /opt/memegen/
COPY CHANGELOG.md /opt/memegen/CHANGELOG.md

# Install Python Requirements
RUN pip install wheel && \
    pip install -r /opt/memegen/requirements.txt

# Set the environment variables
ENV PATH="/opt/memegen/.local/bin:${PATH}"
ENV PORT="${ARG_PORT:-5000}"
ENV MAX_REQUESTS="${ARG_MAX_REQUESTS:-0}"
ENV MAX_REQUESTS_JITTER="${ARG_MAX_REQUESTS_JITTER:-0}"

# Set the entrypoint
ENTRYPOINT gunicorn --bind "0.0.0.0:$PORT" \
    --worker-class uvicorn.workers.UvicornWorker  \
    --max-requests="$MAX_REQUESTS" \
    --max-requests-jitter="$MAX_REQUESTS_JITTER" \
    --timeout=20  \
    app.main:app

CONTRIBUTING.md METASEP
# Setup

## Requirements

- Make:
  - macOS: `$ xcode-select --install`
  - Linux: [https://www.gnu.org/software/make](https://www.gnu.org/software/make)
  - Windows: [https://mingw.org/download/installer](https://mingw.org/download/installer)
- Python: `$ pyenv install`
- Poetry: [https://poetry.eustace.io/docs/#installation](https://poetry.eustace.io/docs/#installation)

To confirm these system dependencies are configured correctly:

```text
$ make doctor
```

## Installation

Install project dependencies into a virtual environment:

```text
$ make install
```

# Local Development

To start the API server and static site:

```text
$ make run
```

## Preview image rendering

View all example images: http://localhost:5000/examples

View test images with automatic reload: http://localhost:5000/test

To view a specific image with automatic reload, simply drop the file extension.

## Add a new meme template

1. Visit http://localhost:5000/<my_new_template_id>
2. Add a `default.png` (or JPG) background image in `templates/<my_new_template_id>` directory
3. Update `config.yml` in the `templates/<my_new_template_id>` directory
4. Refresh http://localhost:5000/images/<my_new_template_id> to see the example
5. Adjust `config.yml` as necessary to position and style the text
6. Visit http://localhost:5000/templates to validate all templates

# Continuous Integration

## Manual

Run the tests:

```text
$ make test
```

Run static analysis:

```text
$ make check
```

## Automatic

Keep all of the above tasks running on change:

```text
$ make dev
```

# Deployment

## Heroku

This service is built to run on Heroku. Route traffic through a CDN to cache generated images.

## Containerization

You can also build this service as a container simply by running either of the following commands:

```bash
# Using Podman:
podman build -t memegen -f Containerfile .

# Using Docker:
docker build -t memegen -f Containerfile .
```

You can then run the container by running:

```bash
# Run the container
podman run -p 5000:5000 -e DOMAIN="set.your.domain" memegen

# Validate functionality
$ curl http://127.0.0.1:5000/docs/ -sI
HTTP/1.1 200 OK
date: Sun, 24 Oct 2021 19:16:07 GMT
server: uvicorn
last-modified: Sun, 24 Oct 2021 18:27:30 GMT
content-type: text/html; charset=utf-8
access-control-allow-origin: *
```

### Multi-Architecture Container Builds

If you want to build the image for multiple CPU architectures, you can either use `buildah` or `docker buildx`. Below are examples for both, and assume you're in the root of the memegen git repo.

#### System Requirements

On Debian/Ubuntu, you'll need to run:

```bash
sudo apt install -y podman buildah qemu-user-static
```

On any RHEL flavor OS, you'll need to run:

```bash
sudo yum install -y podman buildah qemu-user-static
```

On Arch or Manjaro Linux, you'll need to run:

```bash
sudo pacman -Sy podman buildah qemu-arch-extra
```

#### `docker buildx` Instructions

> This assumes you have [docker buildx](https://docs.docker.com/buildx/working-with-buildx/) already installed!

```bash
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    --tag "ghcr.io/${GITHUB_USERNAME}/memegen:latest" \
    --push .
```

#### `buildah` Instructions

```bash
# Create a multi-architecture manifest
buildah manifest create memegen

# Build your amd64 architecture container
buildah bud \
    --tag "ghcr.io/${GITHUB_USERNAME}/memegen:latest" \
    --manifest memegen \
    --arch amd64 .

# Build your arm64 architecture container
buildah bud \
    --tag "ghcr.io/${GITHUB_USERNAME}/memegen:latest" \
    --manifest memegen \
    --arch arm64 .

# Enter your GitHub Personal Access Token below
read -s GITHUB_PERSONAL_ACCESS_TOKEN
# Log into your GitHub Container Registry
echo "${GITHUB_PERSONAL_ACCESS_TOKEN}" | buildah login --username danmanners --password-stdin ghcr.io

# Push the full manifest, with both CPU Architectures
buildah manifest push --all memegen \
  "docker://ghcr.io/${GITHUB_USERNAME}/memegen:latest"
```

> Please note that building the `--arch arm` build on an `amd64` (Intel/AMD) system can take nearly an hour.

CHANGELOG.md METASEP
# Revision History

## 10.5

- Added `font` parameter to override a template's font.
- Added a `/fonts` API to list the available fonts.

## 10.4

- Added `frames` parameter to manually reduce GIF sizes.

## 10.3

- Added support to `start`/`stop` text in GIFs.

## 10.2

- Added support for rotated overlay images.

## 10.1

- Added support for multiple overlays via comma-separated URLs in the `style` parameter.
- Added `overlays` count in `/templates` responses.

## 10.0

- Replaced `example` field in `/templates` responses with `{ "text": [str] , "url": str }`.

## 9.3

- Added `confidence` ratio to results from `/images/automatic`.

## 9.2

- Added redirect to `.gif` format when `style=animated` parameter is present.
- Added `animated` parameter to `/templates` to find templates supporting animation.

## 9.1

- Added escape characters for `<` and `>` as `~l` and `~g`, respectively.

## 9.0

- Renamed `image_url` body parameter to `background` to match the URL value.

## 8.5

- Added support for custom overlays when the `style` parameter is included in image URLs.

## 8.4

- Updated `/images/custom` to return previous results with `GET` requests.

## 8.3

- Added `filter` parameter to `/images` and `/templates` to query for matching names and examples.

## 8.2

- Added `/images/custom` as an alias for `/templates/custom`.
- Removed `POST /templates/*` from the documentation to clarify routes.

## 8.1

- Updated APIs that accept `text_lines[]` to support `/` as a placeholder for blank lines.

## 8.0

- Renamed `template_key` to `template_id` in APIs.

## 7.1

- Added support for the `X-API-KEY` header to disable watermarks.

## 7.0

- Renamed `sample` to `example` in template API responses.

## 6.3

- Added `lines` to `/templates/<key>` responses to indicate the maximum number of supported lines of text.

## 6.2

- Added `/images/preview.jpg` route for quicker responses when clients want to show live previews of a custom meme.

## 6.1

- Added `extension` parameter to specify image format when using POST endpoints.

## 6.0

- Initial release of the API rewrite using Sanic.

## 5.x

- Prior history: https://github.com/jacebrowning/memegen-flask/blob/main/CHANGELOG.md

app/utils/urls.py METASEP
from urllib.parse import unquote, urlencode

from furl import furl

from .. import settings

FLAGS = {
    "0": False,
    "1": True,
    "false": False,
    "no": False,
    "true": True,
    "yes": True,
}


def schema(value) -> bool:
    return value and "://" in value


def arg(data: dict, default, *names: str):
    for name in names:
        value = data.get(name)
        if value is not None:
            return value
    return default


def flag(request, name, default=None):
    value = request.args.get(name, "").lower()
    return FLAGS.get(value, default)


def add(url: str, **kwargs):
    joiner = "&" if "?" in url else "?"
    return url + joiner + urlencode(kwargs)


def normalize(url: str) -> str:
    original = furl(url)
    normalized = furl(f"{settings.BASE_URL}{original.path}")

    if "background" in original.args:
        normalized.args["background"] = original.args["background"]

    return clean(str(normalized))


def params(**kwargs) -> dict:
    return {k: v for k, v in kwargs.items() if v}


def clean(url: str) -> str:
    # Replace percent-encoded characters
    url = unquote(url)

    # Replace invalid regex escape sequences
    url = url.replace("\\", "~b")

    # Drop trailing spaces
    while "/_." in url:
        url = url.replace("/_.", ".")

    return url

app/utils/text.py METASEP
import hashlib
import re
from urllib.parse import unquote


def encode(lines: list[str]) -> str:
    encoded_lines = []

    for line in lines:
        if line == "/":
            encoded_lines.append("_")
        elif line:
            encoded = unquote(line)
            for before, after in [
                ("_", "__"),
                ("-", "--"),
                (" ", "_"),
                ("?", "~q"),
                ("%", "~p"),
                ("#", "~h"),
                ('"', "''"),
                ("/", "~s"),
                ("\\", "~b"),
                ("\n", "~n"),
                ("&", "~a"),
                ("<", "~l"),
                (">", "~g"),
                ("‘", "'"),
                ("’", "'"),
                ("“", '"'),
                ("”", '"'),
                ("–", "-"),
            ]:
                encoded = encoded.replace(before, after)
            encoded_lines.append(encoded)
        else:
            encoded_lines.append("_")

    slug = "/".join(encoded_lines)

    return slug or "_"


def decode(slug: str) -> list[str]:
    has_dash = "_----" in slug
    has_arrow = "_--~g" in slug

    slug = slug.replace("_", " ").replace("  ", "_")
    slug = slug.replace("-", " ").replace("  ", "-")
    slug = slug.replace("''", '"')

    if has_dash:
        slug = slug.replace("-- ", " --")
    if has_arrow:
        slug = slug.replace("- ~g", " -~g")

    for before, after in [
        ("~q", "?"),
        ("~p", "%"),
        ("~h", "#"),
        ("~n", "\n"),
        ("~a", "&"),
        ("~l", "<"),
        ("~g", ">"),
        ("~b", "\\"),
    ]:
        slug = slug.replace(before, after)

    lines = slug.split("/")
    lines = [line.replace("~s", "/") for line in lines]

    return lines


def normalize(slug: str) -> tuple[str, bool]:
    slug = unquote(slug)
    normalized_slug = encode(decode(slug))
    return normalized_slug, slug != normalized_slug


def fingerprint(value: str, *, prefix="_custom-", suffix="") -> str:
    return prefix + hashlib.sha1(value.encode()).hexdigest() + suffix


def slugify(value: str) -> str:
    return re.sub(r"[^a-z0-9-]", "", value).strip("-")

app/utils/meta.py METASEP
from pathlib import Path
from urllib.parse import unquote

import aiohttp
from aiocache import cached
from sanic.log import logger

from .. import settings


def version() -> str:
    changelog_lines = Path("CHANGELOG.md").read_text().splitlines()
    version_heading = changelog_lines[2]
    return version_heading.split(" ")[-1]


@cached(ttl=60 * 15 if settings.DEPLOYED else 0)
async def authenticate(request) -> dict:
    info: dict = {}
    if settings.REMOTE_TRACKING_URL:
        api = settings.REMOTE_TRACKING_URL + "auth"
    else:
        return info

    api_key = _get_api_key(request)
    if api_key:
        api_mask = api_key[:2] + "***" + api_key[-2:]
        logger.info(f"Authenticating with API key: {api_mask}")
        async with aiohttp.ClientSession() as session:
            response = await session.get(api, headers={"X-API-KEY": api_key})
            if response.status >= 500:
                settings.REMOTE_TRACKING_ERRORS += 1
                logger.warning(
                    f"Tracker error count: {settings.REMOTE_TRACKING_ERRORS}"
                )
            else:
                info = await response.json()

    return info


@cached(ttl=60 * 15 if settings.DEPLOYED else 0)
async def tokenize(request, url: str) -> tuple[str, bool]:
    api_key = _get_api_key(request) or ""
    token = request.args.get("token")
    default_url = url.replace(f"api_key={api_key}", "").replace("?&", "?").strip("?&")

    if api_key == "myapikey42" and "example.png" not in url:
        logger.warning(f"Example API key used to tokenize: {url}")
        return default_url, True

    if settings.REMOTE_TRACKING_URL:
        api = settings.REMOTE_TRACKING_URL + "tokenize"
    else:
        return url, False

    if api_key or token:
        async with aiohttp.ClientSession() as session:
            response = await session.post(
                api, data={"url": default_url}, headers={"X-API-KEY": api_key}
            )
            if response.status >= 500:
                settings.REMOTE_TRACKING_ERRORS += 1
                logger.warning(
                    f"Tracker error count: {settings.REMOTE_TRACKING_ERRORS}"
                )
                return default_url, False

            data = await response.json()
            return data["url"], data["url"] != url

    return url, False


async def custom_watermarks_allowed(request) -> bool:
    info = await authenticate(request)
    if info.get("image_access", False):
        return True

    token = request.args.get("token")
    if token:
        logger.info(f"Authenticating with token: {token}")
        _url, updated = await tokenize(request, request.url)
        return not updated

    return False


async def get_watermark(request) -> tuple[str, bool]:
    watermark = request.args.get("watermark", "")

    if await custom_watermarks_allowed(request):
        if watermark == settings.DISABLED_WATERMARK:
            return "", False
        return watermark, False

    if watermark:
        if watermark == settings.DEFAULT_WATERMARK:
            logger.warning(f"Redundant watermark: {watermark}")
            return settings.DEFAULT_WATERMARK, True
        if watermark in settings.ALLOWED_WATERMARKS:
            return watermark, False
        logger.warning(f"Invalid watermark: {watermark}")
        return settings.DEFAULT_WATERMARK, True

    return settings.DEFAULT_WATERMARK, False


async def track(request, lines: list[str]):
    if settings.TRACK_REQUESTS and settings.REMOTE_TRACKING_URL:
        api = settings.REMOTE_TRACKING_URL
    else:
        return

    text = " ".join(lines).strip()
    if not text:
        return
    if any(name in request.args for name in ["height", "width", "watermark"]):
        return
    if "localhost" in getattr(request, "host", "localhost"):
        return

    async with aiohttp.ClientSession() as session:
        params = dict(
            text=text,
            referer=_get_referer(request) or settings.BASE_URL,
            result=unquote(request.url),
        )
        logger.info(f"Tracking request: {params}")
        headers = {"X-API-KEY": _get_api_key(request) or ""}
        response = await session.get(api, params=params, headers=headers)
        if response.status != 200:
            try:
                message = await response.json()
            except aiohttp.client_exceptions.ContentTypeError:
                message = await response.text()
            logger.error(f"Tracker response {response.status}: {message}")
        if response.status >= 404 and response.status not in {414, 421, 520}:
            settings.REMOTE_TRACKING_ERRORS += 1
            logger.warning(f"Tracker error count: {settings.REMOTE_TRACKING_ERRORS}")
            if settings.REMOTE_TRACKING_ERRORS >= settings.REMOTE_TRACKING_ERRORS_LIMIT:
                settings.TRACK_REQUESTS = False
                logger.warning(f"Disabled tracking after {response.status} response")


async def search(request, text: str, safe: bool, *, mode="") -> list[dict]:
    if settings.REMOTE_TRACKING_URL:
        api = settings.REMOTE_TRACKING_URL + mode
    else:
        return []

    async with aiohttp.ClientSession() as session:
        params = dict(
            text=text,
            nsfw=0 if safe else 1,
            referer=_get_referer(request) or settings.BASE_URL,
        )
        logger.info(f"Searching for results: {text!r} (safe={safe})")
        headers = {"X-API-KEY": _get_api_key(request) or ""}
        response = await session.get(api, params=params, headers=headers)
        if response.status >= 500:
            settings.REMOTE_TRACKING_ERRORS += 1
            logger.warning(f"Tracker error count: {settings.REMOTE_TRACKING_ERRORS}")
            return []

        data = await response.json()
        if response.status == 200:
            return data

        logger.error(f"Search response: {data}")
        return []


def _get_referer(request):
    return request.headers.get("referer") or request.args.get("referer")


def _get_api_key(request):
    return request.headers.get("x-api-key") or request.args.get("api_key")

app/utils/images.py METASEP
from __future__ import annotations

import io
from pathlib import Path
from typing import Iterator

from PIL import (
    Image,
    ImageDraw,
    ImageFilter,
    ImageFont,
    ImageOps,
    ImageSequence,
    UnidentifiedImageError,
)
from sanic.log import logger

from .. import settings
from ..models import Font, Template, Text
from ..types import Dimensions, Offset, Point

EXCEPTIONS = (
    OSError,
    SyntaxError,
    Image.DecompressionBombError,
    UnidentifiedImageError,
)


def preview(
    template: Template,
    lines: list[str],
    *,
    style: str = settings.DEFAULT_STYLE,
    watermark: str = "",
) -> tuple[bytes, str]:
    path = template.build_path(lines, "", style, settings.PREVIEW_SIZE, "", "jpg")
    logger.info(f"Previewing meme for {path}")
    image = render_image(
        template,
        style,
        lines,
        settings.PREVIEW_SIZE,
        pad=False,
        is_preview=True,
        watermark=watermark,
    )
    stream = io.BytesIO()
    image.convert("RGB").save(stream, format="JPEG", quality=50)
    return stream.getvalue(), "image/jpeg"


def save(
    template: Template,
    lines: list[str],
    watermark: str = "",
    *,
    font_name: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
    style: str = settings.DEFAULT_STYLE,
    size: Dimensions = (0, 0),
    maximum_frames: int = 0,
    directory: Path = settings.IMAGES_DIRECTORY,
) -> Path:
    size = fit_image(*size)

    path = directory / template.build_path(
        lines, font_name, style, size, watermark, extension, maximum_frames
    )
    if path.exists():
        if settings.DEPLOYED:
            logger.info(f"Loading meme from {path}")
            return path
        logger.info(f"Reloading meme at {path}")
    else:
        logger.info(f"Saving meme to {path}")
        path.parent.mkdir(parents=True, exist_ok=True)

    if extension == "gif":
        frames, duration = render_animation(
            template, lines, size, font_name, maximum_frames, watermark=watermark
        )
        frames[0].save(
            path, save_all=True, append_images=frames[1:], duration=duration, loop=0
        )
    else:
        image = render_image(
            template, style, lines, size, font_name, watermark=watermark
        )
        image.convert("RGB").save(path, quality=95)

    return path


def load(path: Path) -> Image:
    image = Image.open(path).convert("RGBA")
    image = ImageOps.exif_transpose(image)
    return image


def embed(template: Template, index: int, foreground_path: Path, background_path: Path):
    try:
        overlay = template.overlay[index]
    except IndexError:
        count = len(template.overlay)
        logger.error(f"Template {template.id!r} only supports {count} overlay(s)")
        overlay = template.overlay[count - 1]

    background = load(background_path)
    foreground = load(foreground_path)

    size = overlay.get_size(background.size)
    foreground.thumbnail(size)
    foreground = foreground.rotate(overlay.angle, expand=True)

    x1, y1, _x2, _y2 = overlay.get_box(background.size, foreground.size)
    background.paste(foreground, (x1, y1), mask=foreground.convert("RGBA"))

    background.convert("RGB").save(background_path)


def render_image(
    template: Template,
    style: str,
    lines: list[str],
    size: Dimensions,
    font_name: str = "",
    *,
    pad: bool | None = None,
    is_preview: bool = False,
    watermark: str = "",
) -> Image:
    background = load(template.get_image(style))

    pad = all(size) if pad is None else pad
    image = resize_image(background, *size, pad, expand=True)
    if any(
        (
            size[0] and size[0] <= settings.PREVIEW_SIZE[0],
            size[1] and size[1] <= settings.PREVIEW_SIZE[1],
        )
    ) and not (is_preview or settings.DEBUG):
        watermark = ""

    for (
        point,
        offset,
        text,
        max_text_size,
        text_fill,
        font,
        stroke_width,
        stroke_fill,
        angle,
    ) in get_image_elements(
        template, lines, font_name, watermark, image.size, is_preview
    ):

        box = Image.new("RGBA", max_text_size)
        draw = ImageDraw.Draw(box)

        if settings.DEBUG:
            xy = (0, 0, max_text_size[0] - 1, max_text_size[1] - 1)
            outline = "orange" if text == settings.PREVIEW_TEXT else "lime"
            draw.rectangle(xy, outline=outline)

        rows = text.count("\n") + 1
        draw.text(
            (-offset[0], -offset[1]),
            text,
            text_fill,
            font,
            spacing=-offset[1] / (rows * 2),
            align="center",
            stroke_width=stroke_width,
            stroke_fill=stroke_fill,
        )

        box = box.rotate(angle, resample=Image.BICUBIC, expand=True)
        image.paste(box, point, box)

    if settings.DEBUG:
        for overlay in template.overlay:
            box = Image.new("RGBA", overlay.get_size(image.size))
            draw = ImageDraw.Draw(box)
            draw.rectangle((0, 0, box.width - 1, box.height - 1), outline="fuchsia")

            # This offset math is inexact, but works well enough to see
            # approximately where rotated overlay images will be placed.
            # TODO: implement a proper solution using trigonometry.
            angle = abs(overlay.angle)
            if angle > 45:
                angle = 90 - angle
            offset = (
                int(angle + (22.5 - angle) / 22.5 - 1),
                int(angle + (22.5 - angle) / 22.5 - 1),
            )
            x1, y1, _x2, _y2 = overlay.get_box(image.size)
            point = (x1 - offset[0], y1 - offset[1])

            box = box.rotate(overlay.angle, expand=True)
            image.paste(box, point, mask=box)

    if pad:
        image = add_blurred_background(image, background, *size)

    if watermark:
        image = add_watermark(image, watermark, is_preview)

    return image


def render_animation(
    template: Template,
    lines: list[str],
    size: Dimensions,
    font_name: str = "",
    maximum_frames: int = 0,
    *,
    pad: bool | None = None,
    is_preview: bool = False,
    watermark: str = "",
) -> tuple[list[Image], int]:
    frames = []

    pad = all(size) if pad is None else pad
    source = Image.open(template.get_image(style="animated"))
    total = getattr(source, "n_frames", 1)
    if maximum_frames >= total:
        modulus = 1.0
    elif maximum_frames:
        modulus = max(1.0, round(total / maximum_frames, 1))
    else:
        scale = min(2.0, settings.DEFAULT_SIZE[1] / size[1] if size[1] else 1.0)
        modulus = max(1.0, round(total / (settings.MAXIMUM_FRAMES * scale), 1))

    if any(
        (
            size[0] and size[0] <= settings.PREVIEW_SIZE[0],
            size[1] and size[1] <= settings.PREVIEW_SIZE[1],
        )
    ) and not (is_preview or settings.DEBUG):
        watermark = ""

    for index, frame in enumerate(ImageSequence.Iterator(source)):
        if (index % modulus) >= 1:
            continue

        stream = io.BytesIO()
        frame.save(stream, format="GIF")
        background = Image.open(stream).convert("RGBA")
        image = resize_image(background, *size, pad, expand=False)

        for (
            point,
            offset,
            text,
            max_text_size,
            text_fill,
            font,
            stroke_width,
            stroke_fill,
            angle,
        ) in get_image_elements(
            template, lines, font_name, watermark, image.size, is_preview, index / total
        ):
            box = Image.new("RGBA", max_text_size)
            draw = ImageDraw.Draw(box)

            if settings.DEBUG:
                xy = (0, 0, max_text_size[0] - 1, max_text_size[1] - 1)
                outline = "orange" if text == settings.PREVIEW_TEXT else "lime"
                draw.rectangle(xy, outline=outline)

            rows = text.count("\n") + 1
            draw.text(
                (-offset[0], -offset[1]),
                text,
                text_fill,
                font,
                spacing=-offset[1] / (rows * 2),
                align="center",
                stroke_width=stroke_width,
                stroke_fill=stroke_fill,
            )

            box = box.rotate(angle, resample=Image.BICUBIC, expand=True)
            image.paste(box, point, box)

        if settings.DEBUG:
            draw = ImageDraw.Draw(image)
            for overlay in template.overlay:
                xy = overlay.get_box(image.size)
                draw.rectangle(xy, outline="fuchsia")

        if pad:
            image = add_blurred_background(image, background, *size)

        if watermark:
            image = add_watermark(image, watermark, is_preview)
        if settings.DEBUG:
            image = add_counter(image, index, total, modulus)

        frames.append(image)

    ratio = len(frames) / max(total, settings.MAXIMUM_FRAMES)
    duration = source.info.get("duration", 100) / ratio

    return frames, duration


def resize_image(
    image: Image, width: int, height: int, pad: bool, *, expand: bool
) -> Image:
    ratio = image.width / image.height
    default_width, default_height = settings.DEFAULT_SIZE

    if pad:
        if width < height * ratio:
            size = width, int(width / ratio)
        else:
            size = int(height * ratio), height
    elif width:
        size = width, int(width / ratio)
    elif height:
        size = int(height * ratio), height
    elif ratio < 1.0:
        if expand:
            size = default_width, int(default_height / ratio)
        else:
            size = int(default_width * ratio), default_height
    else:
        if expand:
            size = int(default_width * ratio), default_height
        else:
            size = default_width, int(default_height / ratio)

    image = image.resize(size, Image.LANCZOS)
    return image


def fit_image(width: float, height: float) -> tuple[int, int]:
    while width * height > settings.MAXIMUM_PIXELS:
        width *= 0.75
        height *= 0.75
    return int(width), int(height)


def add_blurred_background(
    foreground: Image, background: Image, width: int, height: int
) -> Image:
    base_width, base_height = foreground.size

    border_width = min(width, base_width + 2)
    border_height = min(height, base_height + 2)
    border_dimensions = border_width, border_height
    border = Image.new("RGB", border_dimensions)
    border.paste(
        foreground,
        ((border_width - base_width) // 2, (border_height - base_height) // 2),
    )

    padded = background.resize((width, height), Image.LANCZOS)
    darkened = padded.point(lambda p: int(p * 0.4))
    blurred = darkened.filter(ImageFilter.GaussianBlur(5))

    blurred_width, blurred_height = blurred.size
    offset = (
        (blurred_width - border_width) // 2,
        (blurred_height - border_height) // 2,
    )
    blurred.paste(border, offset)

    return blurred


def add_watermark(image: Image, text: str, is_preview: bool) -> Image:
    size = (image.size[0], settings.WATERMARK_HEIGHT)
    font = get_font("tiny", text, 0.0, size, 99)
    offset = get_text_offset(text, font, size)

    watermark = Text.get_error() if is_preview else Text.get_watermark()
    stroke_width = get_stroke_width(font)
    stroke_width, stroke_fill = watermark.get_stroke(stroke_width)

    box = Image.new("RGBA", image.size)
    draw = ImageDraw.Draw(box)
    draw.text(
        (3, image.size[1] - size[1] - offset[1] - 1),
        text,
        watermark.color,
        font,
        stroke_width=stroke_width,
        stroke_fill=stroke_fill,
    )

    return Image.alpha_composite(image, box)


def add_counter(image: Image, index: int, total: int, modulus: float) -> Image:
    size = (image.size[0], settings.WATERMARK_HEIGHT)
    text = f"{index+1:02} of {total:02} / {modulus}"
    font = get_font("tiny", text, 0.0, size, 99)

    box = Image.new("RGBA", image.size)
    draw = ImageDraw.Draw(box)
    draw.text((3, -3), text, "lime", font, stroke_width=1, stroke_fill="black")

    return Image.alpha_composite(image, box)


def get_image_elements(
    template: Template,
    lines: list[str],
    font_name: str,
    watermark: str,
    image_size: Dimensions,
    is_preview: bool = False,
    percent_rendered: float | None = None,
) -> Iterator[tuple[Point, Offset, str, Dimensions, str, ImageFont, int, str, float]]:
    for index, text in enumerate(template.text):
        if percent_rendered is None:
            yield get_image_element(
                lines, index, text, font_name, image_size, watermark
            )
        elif not text.stop or (text.start <= percent_rendered <= text.stop):
            yield get_image_element(
                lines, index, text, font_name, image_size, watermark
            )
        else:
            yield get_image_element([], index, text, font_name, image_size, watermark)
    if is_preview:
        lines = [settings.PREVIEW_TEXT]
        index = 0
        text = Text.get_preview()
        yield get_image_element(lines, index, text, "", image_size, watermark)


def get_image_element(
    lines: list[str],
    index: int,
    text: Text,
    font_name: str,
    image_size: Dimensions,
    watermark: str,
) -> tuple[Point, Offset, str, Dimensions, str, ImageFont, int, str, float]:
    point = text.get_anchor(image_size, watermark)

    max_text_size = text.get_size(image_size)
    max_font_size = int(image_size[1] / (4 if text.angle else 9))

    try:
        line = lines[index]
    except IndexError:
        line = ""
    else:
        line = text.stylize(
            wrap(font_name or text.font, line, max_text_size, max_font_size),
            lines=lines,
        )

    font = get_font(
        font_name or text.font, line, text.angle, max_text_size, max_font_size
    )
    offset = get_text_offset(line, font, max_text_size)

    stroke_width, stroke_fill = text.get_stroke(get_stroke_width(font))

    return (
        point,
        offset,
        line,
        max_text_size,
        text.color,
        font,
        stroke_width,
        stroke_fill,
        text.angle,
    )


def wrap(font: str, line: str, max_text_size: Dimensions, max_font_size: int) -> str:
    lines_1 = line
    lines_2 = split_2(line)
    lines_3 = split_3(line)

    font_1 = get_font(font, lines_1, 0, max_text_size, max_font_size)
    font_2 = get_font(font, lines_2, 0, max_text_size, max_font_size)
    font_3 = get_font(font, lines_3, 0, max_text_size, max_font_size)

    if font_1.size == font_2.size and font_2.size <= settings.MINIMUM_FONT_SIZE:
        return lines_2

    if font_1.size >= font_2.size:
        return lines_1

    if get_text_size(lines_3, font_3)[0] >= max_text_size[0] * 0.60:
        return lines_3

    if get_text_size(lines_2, font_2)[0] >= max_text_size[0] * 0.60:
        return lines_2

    return lines_1


def split_2(line: str) -> str:
    midpoint = len(line) // 2 - 1

    for offset in range(0, len(line) // 4):
        for index in [midpoint - offset, midpoint + offset]:
            if line[index] == " ":
                return line[:index].strip() + "\n" + line[index:].strip()

    return line


def split_3(line: str) -> str:
    max_len = len(line) / 3
    words = line.split(" ")
    lines = ["", "", ""]
    index = 0

    for word in words:
        current_len = len(lines[index])
        next_len = current_len + len(word) * 0.7
        if next_len > max_len:
            if index < 2:
                index += 1

        lines[index] += word + " "

    return "\n".join(lines).strip()


def get_font(
    name: str,
    text: str,
    angle: float,
    max_text_size: Dimensions,
    max_font_size: int,
) -> ImageFont:
    font_path = Font.objects.get(name or settings.DEFAULT_FONT).path
    max_text_width = max_text_size[0] - max_text_size[0] / 35
    max_text_height = max_text_size[1] - max_text_size[1] / 10

    for size in range(max(settings.MINIMUM_FONT_SIZE, max_font_size), 6, -1):
        font = ImageFont.truetype(str(font_path), size=size)
        text_width, text_height = get_text_size_minus_font_offset(text, font)
        if text_width <= max_text_width and text_height <= max_text_height:
            break

    return font


def get_text_size_minus_font_offset(text: str, font: ImageFont) -> Dimensions:
    text_width, text_height = get_text_size(text, font)
    offset = font.getoffset(text)
    return text_width - offset[0], text_height - offset[1]


def get_text_offset(text: str, font: ImageFont, max_text_size: Dimensions) -> Offset:
    text_size = get_text_size(text, font)
    stroke_width = get_stroke_width(font)

    x_offset, y_offset = font.getoffset(text)

    x_offset -= stroke_width
    y_offset -= stroke_width

    rows = text.count("\n") + 1
    if rows >= 3:
        y_adjust = 1.1
    else:
        y_adjust = 1 + (3 - rows) * 0.25

    x_offset -= (max_text_size[0] - text_size[0]) / 2
    y_offset -= (max_text_size[1] - text_size[1] / y_adjust) / 2

    return x_offset, y_offset


def get_text_size(text: str, font: ImageFont) -> Dimensions:
    image = Image.new("RGB", (100, 100))
    draw = ImageDraw.Draw(image)
    text_size = draw.textsize(text, font)
    stroke_width = get_stroke_width(font)
    return text_size[0] + stroke_width, text_size[1] + stroke_width


def get_stroke_width(font: ImageFont) -> int:
    return min(3, max(1, font.size // 12))

app/utils/http.py METASEP
import asyncio

import aiofiles
import aiohttp
import aiohttp.client_exceptions
from aiopath import AsyncPath
from sanic.log import logger

EXCEPTIONS = (
    aiohttp.client_exceptions.ClientConnectionError,
    aiohttp.client_exceptions.InvalidURL,
    aiohttp.client_exceptions.TooManyRedirects,
    AssertionError,
    asyncio.TimeoutError,
    UnicodeError,
)


async def download(url: str, path: AsyncPath) -> bool:
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get(url, timeout=10) as response:

                if response.status == 200:
                    f = await aiofiles.open(path, mode="wb")  # type: ignore
                    await f.write(await response.read())
                    await f.close()
                    return True

                logger.error(f"{response.status} response from {url}")

        except EXCEPTIONS as e:
            message = str(e).strip("() ") or e.__class__.__name__
            logger.error(f"Invalid response from {url}: {message}")

    return False

app/utils/html.py METASEP
from .. import settings

COLUMNS_STYLE = """
<style>
#images {
   /* Prevent vertical gaps */
   line-height: 0;

   -webkit-column-count: 6;
   -webkit-column-gap:   0px;
   -moz-column-count:    6;
   -moz-column-gap:      0px;
   column-count:         6;
   column-gap:           0px;
}

#images img {
  /* Just in case there are inline attributes */
  width: 100% !important;
  height: auto !important;
}

@media (max-width: 1140px) {
  #images {
  -moz-column-count:    5;
  -webkit-column-count: 5;
  column-count:         5;
  }
}
@media (max-width: 960px) {
  #images {
  -moz-column-count:    4;
  -webkit-column-count: 4;
  column-count:         4;
  }
}
@media (max-width: 720px) {
  #images {
  -moz-column-count:    3;
  -webkit-column-count: 3;
  column-count:         3;
  }
}
@media (max-width: 540px) {
  #images {
  -moz-column-count:    2;
  -webkit-column-count: 2;
  column-count:         2;
  }
}

body {
  margin: 0;
  padding: 0;
}
</style>
""".strip()


REFRESH_SCRIPT = r"""
<script>
    setInterval(function() {
        var images = document.images;
        for (var i=0; i<images.length; i++) {
            images[i].src = images[i].src.replace(
                /\btime=[^&]*/, 'time=' + new Date().getTime()
            );
        }
    }, {interval});
</script>
"""

RESIZE_SCRIPT = """
<script
    src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.2.11/iframeResizer.contentWindow.js"
    integrity="sha512-RMBWitJB1ymY4l6xeYsFwoEgVCAnOWX/zL1gNwXjlUj78nZ8SVbJsZxbH/w0p2jDNraHkOW8rzQgcJ0LNSXWBA=="
    crossorigin="anonymous">
</script>
"""

HTML = """
<!doctype html>
<html>
<head>
{head}
</head>
<body>
{body}
</body>
</html>
"""


def gallery(
    urls: list[str],
    *,
    columns: bool,
    refresh: int,
    query_string: str = "",
) -> str:
    extra = "&" + query_string if query_string else ""
    if columns:
        if refresh:
            return _columns_debug(urls, refresh, extra)
        return _columns(urls)
    assert refresh
    return _grid_debug(urls, refresh, extra)


def _columns(urls: list[str]) -> str:
    elements = []

    for url in urls:
        elements.append(
            f"""
            <a href="https://memecomplete.com/edit/{url}" target="_parent">
                <img src="{url}?width={settings.PREVIEW_SIZE[0]}&frames=10">
            </a>
            """
        )

    elements.append(RESIZE_SCRIPT)

    images = "\n".join(elements).replace("\n" + " " * 12, "\n")

    head = "<title>Memegen.link | examples</title>\n" + COLUMNS_STYLE
    body = f'<section id="images">\n{images}\n</section>'

    return HTML.format(head=head, body=body)


def _columns_debug(urls: list[str], refresh: int, extra: str) -> str:
    elements = []

    for url in urls:
        elements.append(
            f"""
            <a href="{url}">
                <img src="{url}?width={settings.PREVIEW_SIZE[0]}&time=0{extra}">
            </a>
            """
        )

    if refresh:
        elements.append(REFRESH_SCRIPT.replace("{interval}", str(refresh * 1000)))

    images = "\n".join(elements).replace("\n" + " " * 12, "\n")

    head = "<title>Memegen.link | debug</title>\n" + COLUMNS_STYLE
    body = f'<section id="images">\n{images}\n</section>'

    return HTML.format(head=head, body=body)


def _grid_debug(urls: list[str], refresh: int, extra: str):
    elements = []

    for url in urls:
        elements.append(
            f"""
            <a href="{url}">
                <img src="{url}?time=0{extra}">
            </a>
            """
        )

    elements.append(REFRESH_SCRIPT.replace("{interval}", str(refresh * 1000)))

    images = "\n".join(elements).replace("\n" + " " * 12, "\n")

    head = "<title>Memegen.link | test</title>\n"
    body = images

    return HTML.format(head=head, body=body)

app/utils/__init__.py METASEP
from . import html, http, images, meta, text, urls

app/tests/test_views_templates.py METASEP
import json

import pytest


def describe_list():
    def describe_GET():
        @pytest.mark.slow
        @pytest.mark.parametrize("slash", ["", "/"])
        def it_returns_all_templates(expect, client, slash):
            request, response = client.get("/templates" + slash, timeout=10)
            expect(response.status) == 200
            expect(len(response.json)) >= 140

        @pytest.mark.slow
        def it_can_filter_templates(expect, client):
            request, response = client.get("/templates?filter=awesome", timeout=10)
            expect(response.status) == 200
            expect(len(response.json)) == 3


def describe_detail():
    def describe_GET():
        @pytest.mark.parametrize("slash", ["", "/"])
        def it_includes_metadata(expect, client, slash):
            request, response = client.get("/templates/iw" + slash)
            expect(response.status) == 200
            expect(response.json) == {
                "id": "iw",
                "name": "Insanity Wolf",
                "lines": 2,
                "overlays": 1,
                "styles": ["default"],
                "blank": "http://localhost:5000/images/iw.png",
                "example": {
                    "text": ["does testing", "in production"],
                    "url": "http://localhost:5000/images/iw/does_testing/in_production.png",
                },
                "source": "http://knowyourmeme.com/memes/insanity-wolf",
                "_self": "http://localhost:5000/templates/iw",
            }

        def it_returns_404_when_missing(expect, client):
            request, response = client.get("/templates/foobar")
            expect(response.status) == 404

    def describe_POST():
        @pytest.mark.parametrize("as_json", [True, False])
        def it_returns_an_image_url(expect, client, as_json):
            data = {"text_lines[]": ["foo", "bar"], "extension": "jpg"}
            kwargs: dict = {"content": json.dumps(data)} if as_json else {"data": data}
            request, response = client.post("/templates/iw", **kwargs)

            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/iw/foo/bar.jpg"
            }

        @pytest.mark.parametrize("as_json", [True, False])
        def it_supports_custom_backgrounds(expect, client, as_json):
            data = {
                "background": "https://www.gstatic.com/webp/gallery/3.png",
                "text_lines[]": ["foo", "bar"],
                "extension": "jpg",
            }
            kwargs: dict = {"content": json.dumps(data)} if as_json else {"data": data}
            request, response = client.post("/templates/custom", **kwargs)
            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/custom/foo/bar.jpg"
                "?background=https://www.gstatic.com/webp/gallery/3.png"
            }

        @pytest.mark.parametrize("id", ["fry", "custom"])
        def it_redirects_if_requested(expect, client, id):
            data = {"text_lines": ["abc"], "redirect": True}
            request, response = client.post(
                f"/templates/{id}", data=data, allow_redirects=False
            )
            redirect = f"http://localhost:5000/images/{id}/abc.png?status=201"
            expect(response.status) == 302
            expect(response.headers["Location"]) == redirect

        def it_handles_unknown_template_id(expect, client, unknown_template):
            data = {"text_lines": ["one", "two"]}
            request, response = client.post(
                f"/templates/{unknown_template.id}", data=data
            )
            expect(response.status) == 404
            expect(response.json) == {
                "url": "http://localhost:5000/images/unknown/one/two.png"
            }

app/tests/test_views_shortcuts.py METASEP
import pytest

from .. import settings


def describe_image_redirects():
    @pytest.mark.parametrize("extension", ["png", "jpg"])
    def it_redirects_to_normalized_slug(expect, client, extension):
        request, response = client.get(
            f"/images/fry/One Two.{extension}", allow_redirects=False
        )
        expect(response.status) == 301
        expect(response.headers["Location"]) == f"/images/fry/One_Two.{extension}"

    @pytest.mark.parametrize("extension", ["png", "jpg"])
    def it_preserves_query_params_when_redirecting(expect, client, extension):
        request, response = client.get(
            f"/images/custom/One Two.{extension}?alt=http://example.com",
            allow_redirects=False,
        )
        redirect = f"/images/custom/One_Two.{extension}?alt=http://example.com"
        expect(response.status) == 301
        expect(response.headers["Location"]) == redirect

    def it_handles_encoded_newlines(expect, client):
        request, response = client.get("/images/fry/1 2%0A3.jpg", allow_redirects=False)
        redirect = "/images/fry/1_2~n3.jpg"
        expect(response.status) == 301
        expect(response.headers["Location"]) == redirect


def describe_path_redirects():
    def it_redirects_to_example_image_when_no_extension(expect, client):
        request, response = client.get("/images/fry", allow_redirects=False)
        redirect = "/images/fry/not_sure_if_trolling/or_just_stupid.png"
        expect(response.status) == 302
        expect(response.headers["Location"]) == redirect

    def it_redirects_to_custom_image_when_text_but_no_extension(expect, client):
        request, response = client.get("/images/fry/_XD\\XD", allow_redirects=False)
        expect(response.status) == 302
        expect(response.headers["Location"]) == "/images/fry/_XD~bXD.png"

    def it_returns_gallery_view_when_debug(expect, client, monkeypatch):
        monkeypatch.setattr(settings, "DEBUG", True)
        request, response = client.get("/images/fry/test")
        expect(response.text).contains("/images/fry/test.png")

    def it_rejects_unknown_templates(expect, client, unknown_template):
        request, response = client.get(
            f"/images/{unknown_template.id}", allow_redirects=False
        )
        expect(response.status) == 404

    def it_creates_new_templates_when_debug(
        expect, client, unknown_template, monkeypatch
    ):
        monkeypatch.setattr(settings, "DEBUG", True)
        request, response = client.get(
            f"/images/{unknown_template.id}", allow_redirects=False
        )
        expect(response.status) == 501
        expect(response.text).contains("Template not fully implemented")

    def it_handles_sample_templates(expect, client, monkeypatch):
        monkeypatch.setattr(settings, "DEBUG", True)
        request, response = client.get("/images/<sample>", allow_redirects=False)
        expect(response.status) == 501
        expect(response.text).contains("Replace '&lt;sample>' in the URL")

    def it_handles_trailing_slashes(expect, client):
        request, response = client.get("/images/fry/", allow_redirects=False)
        redirect = "/images/fry"
        expect(response.status) == 302
        expect(response.headers["Location"]) == redirect


def describe_legacy_images():
    @pytest.mark.parametrize("extension", ["png", "jpg"])
    def it_redirects_to_example_image(expect, client, extension):
        request, response = client.get(f"/fry.{extension}", allow_redirects=False)
        redirect = f"/images/fry/not_sure_if_trolling/or_just_stupid.{extension}"
        expect(response.status) == 302
        expect(response.headers["Location"]) == redirect

    @pytest.mark.parametrize("extension", ["png", "jpg"])
    def it_redirects_to_custom_image(expect, client, extension):
        request, response = client.get(f"/fry/test.{extension}", allow_redirects=False)
        expect(response.status) == 302
        expect(response.headers["Location"]) == f"/images/fry/test.{extension}"


def describe_legacy_paths():
    @pytest.mark.parametrize("suffix", ["", ".png", ".jpg"])
    def it_rejects_unknown_templates(expect, client, unknown_template, suffix):
        request, response = client.get(f"/{unknown_template.id}{suffix}")
        expect(response.status) == 404

    @pytest.mark.parametrize("suffix", ["", ".png", ".jpg"])
    def it_rejects_unknown_templates_with_text(
        expect, client, unknown_template, suffix
    ):
        request, response = client.get(f"/{unknown_template.id}/test{suffix}")
        expect(response.status) == 404


def describe_legacy_params():
    @pytest.mark.slow
    def it_accepts_alt_for_template(expect, client):
        request, response = client.get(
            "/images/custom/test.png?alt=https://www.gstatic.com/webp/gallery/3.jpg"
        )
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/png"

    @pytest.mark.slow
    def it_accepts_alt_for_style(expect, client):
        request, response = client.get("/images/sad-biden/test.png?style=scowl")
        expect(response.status) == 200

        request, response2 = client.get("/images/sad-biden/test.png?alt=scowl")
        expect(response.status) == 200

        expect(len(response.content)) == len(response2.content)

app/tests/test_views_memes.py METASEP
import json
from unittest.mock import AsyncMock, patch

import pytest

from .. import settings


def describe_list():
    def describe_GET():
        @pytest.mark.slow
        def it_returns_example_image_urls(expect, client):
            request, response = client.get("/images", timeout=10)
            expect(response.status) == 200
            expect(response.json).contains(
                {
                    "url": "http://localhost:5000/images/iw/does_testing/in_production.png",
                    "template": "http://localhost:5000/templates/iw",
                }
            )

        @pytest.mark.slow
        def it_can_filter_examples(expect, client):
            request, response = client.get("/images?filter=awesome", timeout=10)
            expect(response.status) == 200
            expect(len(response.json)) == 3

    def describe_POST():
        @pytest.mark.parametrize("as_json", [True, False])
        def it_returns_an_image_url(expect, client, as_json):
            data = {"template_id": "iw", "text_lines[]": ["foo", "bar"]}
            kwargs: dict = {"content": json.dumps(data)} if as_json else {"data": data}
            request, response = client.post("/images", **kwargs)
            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/iw/foo/bar.png"
            }

        def it_removes_redundant_styles(expect, client):
            data = {
                "template_id": "iw",
                "text_lines[]": ["foo", "bar"],
                "style[]": [" ", "test", "default"],
                "font": "impact",
            }
            request, response = client.post("/images", data=data)
            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/iw/foo/bar.png?style=default,test&font=impact"
            }

        def it_returns_gif_when_animated(expect, client):
            data = {
                "template_id": "iw",
                "text_lines[]": ["foo", "bar"],
                "style": "animated",
            }
            request, response = client.post("/images", data=data)
            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/iw/foo/bar.gif"
            }

        def it_redirects_if_requested(expect, client):
            data = {"template_id": "iw", "text_lines": ["abc"], "redirect": True}
            request, response = client.post("/images", data=data, allow_redirects=False)
            redirect = "http://localhost:5000/images/iw/abc.png?status=201"
            expect(response.status) == 302
            expect(response.headers["Location"]) == redirect

        def it_requires_template_id(expect, client):
            data = {"text_lines": ["foo", "bar"]}
            request, response = client.post("/images", data=data)
            expect(response.status) == 400
            expect(response.json) == {"error": '"template_id" is required'}

        def it_handles_unknown_template_id(expect, client, unknown_template):
            data = {"template_id": unknown_template.id, "text_lines": ["one", "two"]}
            request, response = client.post("/images", data=data)
            expect(response.status) == 404
            expect(response.json) == {
                "url": "http://localhost:5000/images/unknown/one/two.png"
            }

        def it_handles_unknown_template_id_redirect(expect, client, unknown_template):
            data = {
                "template_id": unknown_template.id,
                "text_lines": ["one", "two"],
                "redirect": True,
            }
            request, response = client.post("/images", data=data, allow_redirects=False)
            redirect = "http://localhost:5000/images/unknown/one/two.png?status=201"
            expect(response.status) == 302
            expect(response.headers["Location"]) == redirect

        def it_handles_missing_text_lines(expect, client):
            data = {"template_id": "iw"}
            request, response = client.post("/images", data=data)
            expect(response.status) == 201
            expect(response.json) == {"url": "http://localhost:5000/images/iw.png"}

        def it_drops_trailing_blank_lines(expect, client):
            data = {"template_id": "iw", "text_lines": ["", "", "", ""]}
            request, response = client.post("/images", data=data)
            expect(response.status) == 201
            expect(response.json) == {"url": "http://localhost:5000/images/iw.png"}

        def it_supports_slashes_to_indicate_blank_lines(expect, client):
            data = {"template_id": "iw", "text_lines": ["/", "2", "/", ""]}
            request, response = client.post("/images", data=data)
            expect(response.status) == 201
            expect(response.json) == {"url": "http://localhost:5000/images/iw/_/2.png"}

        def it_handles_invalid_json(expect, client):
            request, response = client.post("/images", content="???")
            expect(response.status) == 400
            expect(response.json) == {"error": '"template_id" is required'}


def describe_detail():
    @pytest.mark.slow
    @pytest.mark.parametrize(
        ("path", "content_type"),
        [
            ("/images/fry.gif", "image/gif"),
            ("/images/fry.jpg", "image/jpeg"),
            ("/images/fry.png", "image/png"),
            ("/images/fry/test.gif", "image/gif"),
            ("/images/fry/test.jpg", "image/jpeg"),
            ("/images/fry/test.png", "image/png"),
        ],
    )
    def it_returns_an_image(expect, client, path, content_type):
        request, response = client.get(path, timeout=10)
        expect(response.status) == 200
        expect(response.headers["content-type"]) == content_type

    def it_handles_placeholder_templates(expect, client):
        request, response = client.get("/images/string/test.png")
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/png"

    def it_handles_unknown_templates(expect, client, unknown_template):
        request, response = client.get(f"/images/{unknown_template.id}/test.png")
        expect(response.status) == 404
        expect(response.headers["content-type"]) == "image/png"

    def it_rejects_invalid_extensions(expect, client):
        request, response = client.get("/images/fry/test.foobar")
        expect(response.status) == 422
        expect(response.headers["content-type"]) == "image/png"

    def it_rejects_extremely_small_sizes(expect, client):
        request, response = client.get("/images/fry/test.jpg?width=9")
        expect(response.status) == 422
        expect(response.headers["content-type"]) == "image/jpeg"

    def it_rejects_invalid_sizes(expect, client):
        request, response = client.get("/images/fry/test.jpg?width=abc")
        expect(response.status) == 422
        expect(response.headers["content-type"]) == "image/jpeg"

    def it_rejects_extremely_long_urls(expect, client):
        text = "test-" * 50
        request, response = client.get(f"/images/fry/{text}.jpg")
        expect(response.status) == 414
        expect(response.headers["content-type"]) == "image/jpeg"

    def describe_font():
        def it_rejects_unknown_fonts(expect, client):
            request, response = client.get("/images/fry/test.png?font=foobar")
            expect(response.status) == 422
            expect(response.headers["content-type"]) == "image/png"

        def it_ignores_placeholder_values(expect, client):
            request, response = client.get("/images/fry/test.png?font=string")
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"

    def describe_watermark():
        @pytest.fixture(autouse=True)
        def watermark_settings(monkeypatch, client):
            monkeypatch.setattr(settings, "ALLOWED_WATERMARKS", ["example.com"])

        @pytest.fixture
        def default_content(watermark_settings, client):
            request, response = client.get("/images/fry/test.png")
            return response.content

        def it_returns_a_unique_image(expect, client, default_content):
            request, response = client.get(
                "/images/fry/test.png?watermark=example.com",
                allow_redirects=False,
            )
            expect(response.status) == 200
            expect(len(response.content)) != len(default_content)

        @pytest.mark.parametrize("extension", ["png", "jpg"])
        def it_rejects_unknown_watermarks(expect, client, extension):
            request, response = client.get(
                f"/images/fry/test.{extension}?watermark=foobar",
                allow_redirects=False,
            )
            expect(response.status) == 302
            expect(response.headers["Location"]) == f"/images/fry/test.{extension}"

        @pytest.mark.parametrize("extension", ["png", "jpg"])
        def it_removes_redundant_watermarks(expect, client, extension):
            request, response = client.get(
                f"/images/fry/test.{extension}?watermark=memegen.link",
                allow_redirects=False,
            )
            expect(response.status) == 302
            expect(response.headers["Location"]) == f"/images/fry/test.{extension}"

        @patch(
            "app.utils.meta.authenticate",
            AsyncMock(return_value={"image_access": True}),
        )
        def it_accepts_custom_values_when_authenticated(expect, client):
            request, response = client.get(
                "/images/fry/test.png?watermark=mydomain.com",
                allow_redirects=False,
            )
            expect(response.status) == 200

        def it_rejects_invalid_authentication(expect, client):
            request, response = client.get(
                "/images/fry/test.png?watermark=blank",
                headers={"X-API-KEY": "foobar"},
                allow_redirects=False,
            )
            expect(response.status) == 302
            expect(response.headers["Location"]) == "/images/fry/test.png"

        def it_is_disabled_automatically_for_small_images(expect, client):
            small_content = client.get("/images/fry/test.png?width=300")[1].content
            request, response = client.get(
                "/images/fry/test.png?width=300&watermark=example.com",
                allow_redirects=False,
            )
            expect(response.status) == 200
            expect(len(response.content)) == len(small_content)

    def describe_styles():
        @pytest.fixture(
            params=[
                "/images/ds/one/two.png?",
                "/images/custom/test.png?background=https://www.gstatic.com/webp/gallery/3.jpg&",
            ]
        )
        def base_url(request):
            return request.param

        @pytest.mark.slow
        def it_supports_alternate_styles(expect, client):
            request, response = client.get("/images/ds/one/two.png?style=maga")
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"

        @pytest.mark.parametrize("slug", ["ds", "ds/one/two"])
        def it_redirects_to_gif_when_animated(expect, client, slug):
            request, response = client.get(
                f"/images/{slug}.png?style=animated", allow_redirects=False
            )
            redirect = f"/images/{slug}.gif"
            expect(response.status) == 301
            expect(response.headers["Location"]) == redirect

        @pytest.mark.slow
        def it_rejects_invalid_styles(expect, client, base_url):
            request, response = client.get(base_url + "style=foobar")
            expect(response.status) == 422
            expect(response.headers["content-type"]) == "image/png"

        @pytest.mark.slow
        def it_ignores_placeholder_values(expect, client, base_url):
            request, response = client.get(base_url + "style=string")
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"

    def describe_overlay():
        @pytest.fixture(
            params=[
                "/images/fine/test.png?",
                "/images/custom/test.png?background=https://www.gstatic.com/webp/gallery/3.jpg&",
            ]
        )
        def base_url(request):
            return request.param

        def it_supports_custom_styles(expect, client, base_url):
            request, response = client.get(
                base_url + "style=https://www.gstatic.com/webp/gallery/4.jpg"
            )
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"

        @pytest.mark.slow
        def it_requires_image_urls(expect, client, base_url):
            request, response = client.get(base_url + "style=http://example.com")
            expect(response.status) == 415
            expect(response.headers["content-type"]) == "image/png"

        @pytest.mark.slow
        def it_handles_missing_urls(expect, client, base_url):
            request, response = client.get(
                base_url + "style=http://example.com/does_not_exist.png"
            )
            expect(response.status) == 415
            expect(response.headers["content-type"]) == "image/png"

    def describe_custom():
        def it_supports_custom_templates(expect, client):
            request, response = client.get(
                "/images/custom/test.png"
                "?background=https://www.gstatic.com/webp/gallery/3.jpg"
            )
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"

        def it_requires_an_image_with_custom_templates(expect, client):
            request, response = client.get("/images/custom/test.png")
            expect(response.status) == 422
            expect(response.headers["content-type"]) == "image/png"

        def it_handles_invalid_urls(expect, client):
            request, response = client.get("/images/custom/test.png?background=foobar")
            expect(response.status) == 415
            expect(response.headers["content-type"]) == "image/png"

        def it_handles_missing_urls(expect, client):
            request, response = client.get(
                "/images/custom/test.png"
                "?background=http://example.com/does_not_exist.png"
            )
            expect(response.status) == 415
            expect(response.headers["content-type"]) == "image/png"

        def it_ignores_placeholder_values(expect, client):
            request, response = client.get(
                "/images/custom/string.png?background=string"
            )
            expect(response.status) == 200
            expect(response.headers["content-type"]) == "image/png"


def describe_automatic():
    def describe_POST():
        def it_requires_text(expect, client):
            request, response = client.post("/images/automatic")
            expect(response.status) == 400
            expect(response.json) == {"error": '"text" is required'}

        @patch(
            "app.utils.meta.search",
            AsyncMock(
                return_value=[
                    {
                        "image_url": "http://example.com/images/example.png"
                        + "?background=https://www.gstatic.com/webp/gallery/3.png",
                        "confidence": 0.5,
                    }
                ]
            ),
        )
        @pytest.mark.parametrize("as_json", [True, False])
        def it_normalizes_the_url(expect, client, as_json):
            data = {"text": "example"}
            kwargs: dict = {"content": json.dumps(data)} if as_json else {"data": data}
            request, response = client.post("/images/automatic", **kwargs)
            expect(response.json) == {
                "url": "http://localhost:5000/images/example.png"
                + "?background=https://www.gstatic.com/webp/gallery/3.png",
                "confidence": 0.5,
            }

        def it_handles_invalid_json(expect, client):
            request, response = client.post("/images/automatic", content="???")
            expect(response.status) == 400
            expect(response.json) == {"error": '"text" is required'}


def describe_custom():
    def describe_POST():
        @pytest.mark.parametrize("as_json", [True, False])
        def it_supports_custom_backgrounds(expect, client, as_json):
            data = {
                "background": "https://www.gstatic.com/webp/gallery/3.png",
                "text_lines[]": ["foo", "bar"],
                "extension": "jpg",
            }
            kwargs: dict = {"content": json.dumps(data)} if as_json else {"data": data}
            request, response = client.post("/images/custom", **kwargs)
            expect(response.status) == 201
            expect(response.json) == {
                "url": "http://localhost:5000/images/custom/foo/bar.jpg"
                "?background=https://www.gstatic.com/webp/gallery/3.png"
            }

        def it_redirects_if_requested(expect, client):
            data = {
                "background": "https://www.gstatic.com/webp/gallery/4.png",
                "text_lines": ["abc"],
                "redirect": True,
            }
            request, response = client.post(
                "/images/custom", data=data, allow_redirects=False
            )
            redirect = "http://localhost:5000/images/custom/abc.png?background=https://www.gstatic.com/webp/gallery/4.png&status=201"
            expect(response.status) == 302
            expect(response.headers["Location"]) == redirect

    def describe_GET():
        @patch(
            "app.utils.meta.search",
            AsyncMock(
                return_value=[{"image_url": "http://example.com/images/example.png"}]
            ),
        )
        def it_normalizes_the_url(expect, client):
            request, response = client.get("/images/custom")
            expect(response.json) == [
                {"url": "http://localhost:5000/images/example.png"}
            ]

        @patch(
            "app.utils.meta.search",
            AsyncMock(
                return_value=[
                    {
                        "image_url": "http://example.com/images/example.png"
                        + "?background=https://www.gstatic.com/webp/gallery/3.png"
                    }
                ]
            ),
        )
        def it_normalizes_the_url_with_background(expect, client):
            request, response = client.get("/images/custom")
            expect(response.json) == [
                {
                    "url": "http://localhost:5000/images/example.png"
                    + "?background=https://www.gstatic.com/webp/gallery/3.png"
                }
            ]

app/tests/test_views_clients.py METASEP
import pytest


def describe_auth():
    def describe_POST():
        def it_returns_401_when_unauthenticated(expect, client):
            request, response = client.post("/auth")
            expect(response.status) == 401
            expect(response.json) == {"error": "API key missing or invalid."}


def describe_fonts():
    def describe_GET():
        def it_returns_all_fonts(expect, client):
            request, response = client.get("/fonts")
            expect(len(response.json)) == 6


def describe_image_preview():
    @pytest.fixture
    def path():
        return "/images/preview.jpg"

    def it_returns_an_image(expect, client, path):
        request, response = client.get(path)
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/jpeg"

    def it_supports_custom_templates(expect, client, path):
        request, response = client.get(
            path + "?template=https://www.gstatic.com/webp/gallery/1.png"
        )
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/jpeg"

    def it_handles_invalid_urls(expect, client, path):
        request, response = client.get(path + "?template=http://example.com/foobar.jpg")
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/jpeg"

    def it_handles_invalid_keys(expect, client, path, unknown_template):
        request, response = client.get(path + f"?template={unknown_template.id}")
        expect(response.status) == 200
        expect(response.headers["content-type"]) == "image/jpeg"

app/tests/test_utils_text.py METASEP
import pytest

from .. import utils

LINES_SLUG = [
    (["hello world"], "hello_world"),
    (["?%#/&\\<>"], "~q~p~h~s~a~b~l~g"),
    (["a/b", "c"], "a~sb/c"),
    (["variable_name"], "variable__name"),
    (["variable-name"], "variable--name"),
    (["foo\nbar"], "foo~nbar"),
    (["def data() -> Dict"], "def_data()_--~g_Dict"),
    (["finish <- start"], "finish_~l--_start"),
    (['That\'s not how "this" works'], "That's_not_how_''this''_works"),
    (["git commit --no-verify"], "git_commit_----no--verify"),
]


@pytest.mark.parametrize(("lines", "slug"), LINES_SLUG)
def test_encode(expect, lines, slug):
    expect(utils.text.encode(lines)) == slug


@pytest.mark.parametrize(("lines", "slug"), LINES_SLUG)
def test_decode(expect, lines, slug):
    expect(utils.text.decode(slug)) == lines


def test_decode_dashes(expect):
    expect(utils.text.decode("hello-world")) == ["hello world"]


def test_encode_quotes(expect):
    expect(
        utils.text.encode(["it’ll be great “they” said"])
    ) == 'it\'ll_be_great_"they"_said'


def test_encode_dashes(expect):
    expect(utils.text.encode(["1–2 in. of snow"])) == "1-2_in._of_snow"

app/tests/test_utils_meta.py METASEP
import pytest

from .. import settings, utils


def describe_tokenize():
    @pytest.mark.asyncio
    async def it_restricts_sample_api_key_usage(expect, request):
        request.args = {"api_key": "myapikey42"}
        request.headers = {}

        url, updated = await utils.meta.tokenize(
            request, "http://api.memegen.link/images/fry/test.png?api_key=myapikey42"
        )

        expect(url) == "http://api.memegen.link/images/fry/test.png"
        expect(updated) == True


def describe_track():
    @pytest.mark.asyncio
    async def it_is_disabled_automatically_after_error(expect, monkeypatch, request):
        monkeypatch.setattr(settings, "REMOTE_TRACKING_URL", "http://example.com/404")
        monkeypatch.setattr(settings, "REMOTE_TRACKING_ERRORS_LIMIT", 1)
        request.args = {}
        request.headers = {}
        request.host = "example.com"
        request.url = "http://example.com"

        await utils.meta.track(request, ["foo"])
        await utils.meta.track(request, ["bar"])

        expect(settings.TRACK_REQUESTS) == False

app/tests/test_utils_images.py METASEP
import os
import shutil
import time
from pathlib import Path

import pytest

from .. import models, settings, utils


@pytest.fixture(scope="session")
def images():
    path = settings.TEST_IMAGES_DIRECTORY

    flag = path / ".flag"
    if flag.exists():
        age = time.time() - flag.stat().st_mtime
        if age > 60 * 60 * 6 and "SKIP_SLOW" not in os.environ:
            shutil.rmtree(path)

    path.mkdir(exist_ok=True)
    flag.touch()

    return path


@pytest.fixture(scope="session")
def template():
    return models.Template.objects.get("icanhas")


# Formats


@pytest.mark.slow
@pytest.mark.parametrize(("id", "lines", "extension"), settings.TEST_IMAGES)
def test_images(images, id, lines, extension):
    template = models.Template.objects.get(id)
    utils.images.save(template, lines, extension=extension, directory=images)


# Size


def test_smaller_width(images, template):
    utils.images.save(template, ["width=250"], size=(250, 0), directory=images)


def test_smaller_height(images, template):
    utils.images.save(template, ["height=250"], size=(0, 250), directory=images)


def test_larger_width(images, template):
    utils.images.save(template, ["width=500"], size=(500, 0), directory=images)


def test_larger_height(images, template):
    utils.images.save(template, ["height=500"], size=(0, 500), directory=images)


def test_wide_padding(images, template):
    lines = ["width=600", "height=400"]
    utils.images.save(template, lines, size=(600, 400), directory=images)


def test_tall_padding(images, template):
    lines = ["width=400", "height=600"]
    utils.images.save(template, lines, size=(400, 600), directory=images)


def test_small_padding(images, template):
    lines = ["width=50", "height=50"]
    utils.images.save(template, lines, size=(50, 50), directory=images)


@pytest.mark.slow
def test_large_padding(images, template):
    lines = ["width=2000", "height=2000"]
    utils.images.save(template, lines, size=(2000, 2000), directory=images)


# Templates


@pytest.mark.asyncio
async def test_custom_template(images):
    url = "https://www.gstatic.com/webp/gallery/2.jpg"
    template = await models.Template.create(url)
    utils.images.save(template, ["", "My Custom Template"], directory=images)


@pytest.mark.slow
@pytest.mark.asyncio
async def test_custom_template_with_exif_rotation(images):
    url = "https://cdn.discordapp.com/attachments/752902976322142218/752903391281283152/20200608_111430.jpg"
    template = await models.Template.create(url)
    utils.images.save(template, ["", "This should not be rotated!"], directory=images)


def test_unknown_template(images):
    template = models.Template.objects.get("_error")
    utils.images.save(template, ["UNKNOWN TEMPLATE"], directory=images)


# Styles


@pytest.mark.slow
def test_alternate_style(images):
    template = models.Template.objects.get("ds")
    lines = ["one", "two", "three"]
    utils.images.save(template, lines, style="maga", directory=images)


@pytest.mark.slow
@pytest.mark.asyncio
async def test_custom_style(images):
    url = "https://sn56.scholastic.com/content/dam/classroom-magazines/sn56/issues/2019-20/031620/coronavirus/16-SN56-20200316-VirusOutbreak-PO-2.png"
    template = models.Template.objects.get("fine")
    await template.check(url, force=True)
    lines = ["102 °F", "this is fine"]
    utils.images.save(template, lines, style=url, directory=images)


@pytest.mark.slow
@pytest.mark.asyncio
async def test_custom_style_rotated(images):
    style = "https://i.imgur.com/6hwAxmO.jpg,https://i.imgur.com/6hwAxmO.jpg"
    template = models.Template.objects.get("same")
    await template.check(style, force=True)
    utils.images.save(template, [], style=style, directory=images)


# Text


def test_special_characters(images, template):
    lines = ["Special? 100% #these-memes", "template_rating: 9/10"]
    utils.images.save(template, lines, directory=images)


@pytest.mark.skipif("CIRCLECI" in os.environ, reason="Long filenames not supported")
def test_extremely_long_text(images, tmpdir):
    template = models.Template.objects.get("fry")
    lines = ["", "word " * 40]
    utils.images.save(template, lines, directory=Path(tmpdir) / "images")


def test_long_first_word(images):
    template = models.Template.objects.get("fine")
    lines = ["", "thiiiiiiiiiiiiiiiiiiiiis will probably be fine right now"]
    utils.images.save(template, lines, directory=images)


@pytest.mark.slow
def test_text_wrap_when_font_is_too_small(images):
    template = models.Template.objects.get("ds")
    lines = ["this button seems to be ok to push"]
    utils.images.save(template, lines, directory=images)


def test_text_wrap_on_small_images(images):
    template = models.Template.objects.get("pigeon")
    lines = ["", "multiple words here"]
    utils.images.save(template, lines, size=(0, 300), directory=images)


def test_text_wrap_on_smaller_images(images):
    template = models.Template.objects.get("toohigh")
    lines = ["", "the number of sample memes is too damn high!"]
    utils.images.save(template, lines, size=(0, 200), directory=images)


@pytest.mark.slow
def test_descender_vertical_alignment(images):
    template = models.Template.objects.get("ptj")
    lines = [
        "Exit",
        "Exit",
        "the",
        "the",
        "monorepo",
        "monorepo",
        "Exit the monorepo.",
        "Stop testing!",
    ]
    utils.images.save(template, lines, directory=images)


# Fonts


def test_font_override(images, template):
    lines = ["custom", "font"]
    utils.images.save(template, lines, font_name="comic", directory=images)


# Watermark


def test_watermark(images, template):
    lines = ["nominal image", "with watermark"]
    utils.images.save(template, lines, "Example.com", directory=images)


def test_watermark_with_padding(images, template):
    lines = ["paddded image", "with watermark"]
    utils.images.save(template, lines, "Example.com", size=(500, 500), directory=images)


def test_watermark_disabled_when_small(images, template):
    lines = ["small image", "with watermark (disabled)"]
    utils.images.save(template, lines, "Example.com", size=(300, 0), directory=images)


@pytest.mark.slow
def test_watermark_with_many_lines(images):
    template = models.Template.objects.get("ptj")
    lines = ["", "", "", "", "", "", "Has a watermark.", "Doesn't have a watermark!"]
    utils.images.save(template, lines, "Example.com", directory=images)


# Debug


@pytest.mark.parametrize(("extension"), ["png", "gif"])
def test_debug_images(images, monkeypatch, extension):
    monkeypatch.setattr(settings, "DEBUG", True)

    id, lines, _extension = settings.TEST_IMAGES[0]
    template = models.Template.objects.get(id)
    lines = [lines[0], lines[1] + " (debug)"]
    utils.images.save(
        template, lines, directory=images, extension=extension, maximum_frames=5
    )


def test_deployed_images(images, monkeypatch):
    monkeypatch.setattr(settings, "DEPLOYED", True)

    id, lines, _extension = settings.TEST_IMAGES[0]
    template = models.Template.objects.get(id)
    utils.images.save(template, lines, directory=images)

    monkeypatch.delattr(utils.images, "render_image")
    utils.images.save(template, lines, directory=images)


def test_preview_images(images, template):
    path = images / "preview.jpg"
    data, _extension = utils.images.preview(template, ["nominal image", "while typing"])
    path.write_bytes(data)

app/tests/test_models_text.py METASEP
import pytest

from ..models import Text


def describe_text():
    def describe_stylize():
        @pytest.mark.parametrize(
            ("style", "before", "after"),
            [
                ("none", "Hello, world!", "Hello, world!"),
                ("default", "these are words.", "These are words."),
                ("default", "These ARE words.", "These ARE words."),
                ("upper", "Hello, world!", "HELLO, WORLD!"),
                ("lower", "Hello, world!", "hello, world!"),
                ("title", "these are words", "These Are Words"),
                ("capitalize", "these are words", "These are words"),
                ("mock", "these are words", "ThEsE aRe WorDs"),
                ("<unknown>", "Hello, world!", "Hello, world!"),
            ],
        )
        def it_applies_style(expect, style, before, after):
            text = Text()
            text.style = style
            expect(text.stylize(before)) == after

        def it_defaults_to_upper(expect):
            text = Text()
            text.style = ""
            expect(text.stylize("Foobar")) == "FOOBAR"

        def it_respects_case_when_set_in_any_line(expect):
            text = Text(style="default")
            expect(text.stylize("foo", lines=["foo", " ", "bar"])) == "Foo"
            expect(text.stylize("foo", lines=["foo", " ", "Bar"])) == "foo"

app/tests/test_models_template.py METASEP
from pathlib import Path

import datafiles
import log
import pytest

from ..models import Overlay, Template, Text


def describe_template():
    @pytest.fixture
    def template():
        t = Template.objects.get("_test")
        t.clean()
        yield t
        t.clean()

    def describe_str():
        def it_includes_the_path(expect, template):
            expect(str(template)).endswith("/memegen/templates/_test")

    def describe_valid():
        def it_only_includes_default_style_with_custom_overlay(
            expect, template, monkeypatch
        ):
            monkeypatch.setattr(datafiles.settings, "HOOKS_ENABLED", False)

            template.overlay = [Overlay()]
            expect(template.styles) == []

            del template.styles
            template.overlay[0].center_x = 0.123
            expect(template.styles) == ["default"]

    def describe_text():
        def it_defaults_to_two_lines(expect, template):
            expect(template.text) == [Text(), Text(anchor_x=0.0, anchor_y=0.8)]

    def describe_image():
        def it_has_generic_extension_when_absent(expect, template):
            expect(template.image) == Path.cwd() / "templates" / "_test" / "default.img"

        def it_creates_template_directory_automatically(expect):
            template = Template.objects.get_or_create("_custom-empty")
            template.datafile.path.unlink(missing_ok=True)
            template.datafile.path.parent.rmdir()
            log.info(template.image)
            expect(template.datafile.path.parent.exists()) == True

    def describe_create():
        @pytest.mark.asyncio
        async def it_downloads_the_image(expect):
            url = "https://www.gstatic.com/webp/gallery/1.jpg"
            path = (
                Path.cwd()
                / "templates"
                / "_custom-2d3c91e23b91d6387050e85efc1f3acb39b5a95d"
                / "default.jpg"
            )
            template = await Template.create(url, force=True)
            expect(template.image) == path
            expect(template.image.exists()) == True

        @pytest.mark.asyncio
        async def it_handles_missing_urls(expect):
            url = "http://example.com/does_not_exist.png"
            template = await Template.create(url)
            expect(template.image.exists()) == False

        @pytest.mark.asyncio
        async def it_handles_unreachable_urls(expect):
            url = "http://127.0.0.1/does_not_exist.png"
            template = await Template.create(url)
            expect(template.image.exists()) == False

        @pytest.mark.asyncio
        @pytest.mark.parametrize(
            "url",
            [
                "httpshttps://cdn.pixabay.com/photo/2015/09/09/19/41/cat-932846_1280.jpg",
                "https://https://i.imgur.com/bf995.gif&width=400",
            ],
        )
        async def it_handles_invalid_urls(expect, url):
            template = await Template.create(url)
            expect(template.valid) == False

        @pytest.mark.asyncio
        async def it_rejects_non_images(expect):
            url = "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4"
            template = await Template.create(url)
            expect(template.image.exists()) == False

        @pytest.mark.asyncio
        @pytest.mark.parametrize("subdomain", ["api", "staging"])
        async def it_handles_builtin_templates(expect, subdomain):
            url = f"http://{subdomain}.memegen.link/images/fry.png"
            template = await Template.create(url)
            expect(template.id) == "fry"

        @pytest.mark.asyncio
        async def it_handles_invalid_builtin_templates(expect):
            url = "http://api.memegen.link/images/fry2.png"
            template = await Template.create(url)
            expect(template.id) == "_error"

        @pytest.mark.asyncio
        async def it_handles_custom_templates(expect):
            url = "http://api.memegen.link/images/custom.png?background=https://www.gstatic.com/webp/gallery/1.jpg"
            template = await Template.create(url)
            expect(template.id) == "_custom-2d3c91e23b91d6387050e85efc1f3acb39b5a95d"

        @pytest.mark.asyncio
        async def it_handles_custom_templates_lacking_background(expect):
            url = "http://api.memegen.link/images/custom.png?background"
            template = await Template.create(url)
            expect(template.id) == "_error"

        @pytest.mark.asyncio
        async def it_handles_custom_templates_with_invalid_background(expect):
            url = "http://api.memegen.link/images/custom.png?background=https://https://example.com"
            template = await Template.create(url)
            expect(template.id) == "_error"

        @pytest.mark.asyncio
        async def it_handles_meme_urls(expect):
            url = "http://api.memegen.link/images/fry/test.png"
            template = await Template.create(url)
            expect(template.id) == "fry"

    def describe_check():
        @pytest.mark.asyncio
        async def it_determines_overlay_file_extension(expect):
            url = "https://i.guim.co.uk/img/media/8a13052d4db7dcd508af948e5db7b04598e03190/0_294_5616_3370/master/5616.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=bcaa4eed2c1e6dab61c41a61e41433d9"
            template = Template.objects.get("fine")
            expect(await template.check(url, force=True)) == True

        @pytest.mark.asyncio
        async def it_assumes_extension_when_unknown(expect):
            url = "https://camo.githubusercontent.com/ce9c7a173f38722e129d5ae832a11c928ff72683fae74cbcb9fff41fd9957e63/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f7468756d622f332f33662f4769745f69636f6e2e7376672f3130323470782d4769745f69636f6e2e7376672e706e67"
            template = Template.objects.get("fine")
            expect(await template.check(url, force=True)) == True

        @pytest.mark.asyncio
        async def it_accepts_multiple_urls(expect):
            style = ",".join(
                [
                    "https://user-images.githubusercontent.com/674621/71187801-14e60a80-2280-11ea-94c9-e56576f76baf.png",
                    "https://i.stack.imgur.com/PvgbL.png",
                    "https://www.nicepng.com/png/detail/13-139107_notepad-icon-icon-anotacoes-png.png",
                ]
            )
            template = Template.objects.get("perfection")
            expect(await template.check(style)) == True

        @pytest.mark.asyncio
        async def it_accepts_default_style_as_placeholder(expect):
            style = "default,https://i.stack.imgur.com/PvgbL.png"
            template = Template.objects.get("perfection")
            expect(await template.check(style)) == True

app/tests/test_main.py METASEP
import pytest

from .. import settings


def describe_index():
    def it_redirects_to_the_docs(expect, client):
        request, response = client.get("/")
        expect(response.status) == 200
        expect(response.text).contains("openapi.json")

    def it_contains_favicon(expect, client):
        request, response = client.get("/favicon.ico")
        expect(response.status) == 200

    def it_contains_robots(expect, client):
        request, response = client.get("/robots.txt")
        expect(response.status) == 200
        expect(response.text).contains("Allow: /\n")


def describe_examples():
    @pytest.mark.slow
    def it_displays_images(expect, client):
        request, response = client.get("/examples", timeout=10)
        expect(response.status) == 200
        expect(response.text.count("img")) > 100
        expect(response.text).excludes("setInterval")

    @pytest.mark.slow
    def it_can_enable_automatic_refresh(expect, client, monkeypatch):
        monkeypatch.setattr(settings, "DEBUG", True)
        request, response = client.get("/examples?debug=true", timeout=10)
        expect(response.status) == 200
        expect(response.text.count("img")) > 100
        expect(response.text).includes("setInterval")


def describe_test():
    def it_redirects_to_the_index(expect, client):
        request, response = client.get("/test", allow_redirects=False)
        expect(response.status) == 302
        expect(response.headers["Location"]) == "/"

    def it_displays_test_images_when_debug(expect, client, monkeypatch):
        monkeypatch.setattr(settings, "DEBUG", True)
        request, response = client.get("/test", allow_redirects=False)
        expect(response.status) == 200
        expect(response.text.count("img")) > 5
        expect(response.text.count("img")) < 100

app/tests/test_docs.py METASEP
from pkg_resources import get_distribution


def describe_spec():
    def it_contains_the_version(expect, client):
        version = get_distribution("memegen").version
        request, response = client.get("/docs/openapi.json")
        expect(response.status) == 200
        expect(response.json["info"]["version"]) == version

app/tests/conftest.py METASEP
import os

import pytest

from .. import settings
from ..main import app
from ..models import Template


def pytest_configure(config):
    terminal = config.pluginmanager.getplugin("terminal")
    terminal.TerminalReporter.showfspath = False


def pytest_runtest_setup(item):
    for marker in item.iter_markers(name="slow"):
        if "SKIP_SLOW" in os.environ:
            pytest.skip("slow test")


@pytest.fixture
def client(monkeypatch):
    monkeypatch.setattr(settings, "REMOTE_TRACKING_URL", None)
    return app.test_client


@pytest.fixture
def unknown_template():
    template = Template.objects.get_or_create("unknown")
    template.delete()
    yield template
    template.delete()

app/tests/__init__.py METASEP

app/models/text.py METASEP
from dataclasses import dataclass

from sanic.log import logger
from spongemock import spongemock

from .. import settings
from ..types import Dimensions, Point


@dataclass
class Text:

    style: str = "upper"
    color: str = "white"
    font: str = settings.DEFAULT_FONT

    anchor_x: float = 0.0
    anchor_y: float = 0.0

    angle: float = 0

    scale_x: float = 1.0
    scale_y: float = 0.2

    start: float = 0.0
    stop: float = 1.0

    @classmethod
    def get_preview(cls) -> "Text":
        return cls(
            color="#80808060",
            anchor_x=0.075,
            anchor_y=0.05,
            angle=10,
            scale_x=0.75,
            scale_y=0.75,
        )

    @classmethod
    def get_error(cls) -> "Text":
        return cls(color="yellow", anchor_x=0.5)

    @classmethod
    def get_watermark(cls) -> "Text":
        return cls(color="#FFFFFF85")

    def get_anchor(self, image_size: Dimensions, watermark: str = "") -> Point:
        image_width, image_height = image_size
        anchor = int(image_width * self.anchor_x), int(image_height * self.anchor_y)
        if watermark and self.anchor_x <= 0.1 and self.anchor_y >= 0.8:
            anchor = anchor[0], anchor[1] - settings.WATERMARK_HEIGHT // 2
        return anchor

    def get_size(self, image_size: Dimensions) -> Dimensions:
        image_width, image_height = image_size
        size = int(image_width * self.scale_x), int(image_height * self.scale_y)
        return size

    def get_stroke(self, width: int) -> tuple[int, str]:
        if self.color == "black":
            width = 1
            color = "#FFFFFF85"
        elif "#" in self.color:
            width = 1
            color = "#000000" + self.color[-2:]
        else:
            color = "black"
        return width, color

    def stylize(self, text: str, **kwargs) -> str:
        lines = [line for line in kwargs.get("lines", [text]) if line.strip()]

        if self.style == "none":
            return text

        if self.style == "default":
            text = text.capitalize() if all(line.islower() for line in lines) else text
            return text

        if self.style == "mock":
            return spongemock.mock(text, diversity_bias=0.75, random_seed=0)

        method = getattr(text, self.style or self.__class__.style, None)
        if method:
            return method()

        logger.warning(f"Unsupported text style: {self.style}")
        return text

app/models/template.py METASEP
import asyncio
import shutil
from functools import cached_property
from pathlib import Path

import aiopath
from datafiles import datafile, field
from furl import furl
from sanic import Request
from sanic.log import logger

from .. import settings, utils
from ..types import Dimensions
from .overlay import Overlay
from .text import Text


@datafile("../../templates/{self.id}/config.yml", defaults=True)
class Template:

    id: str
    name: str = ""
    source: str | None = None

    text: list[Text] = field(
        default_factory=lambda: [Text(), Text(anchor_x=0.0, anchor_y=0.8)]
    )
    example: list[str] = field(default_factory=lambda: ["Top Line", "Bottom Line"])

    overlay: list[Overlay] = field(default_factory=lambda: [Overlay()])

    def __str__(self):
        return str(self.directory)

    def __lt__(self, other):
        return self.id < other.id

    @cached_property
    def valid(self) -> bool:
        if not settings.DEPLOYED:
            self._update_example()
            self.datafile.save()
        return (
            not self.id.startswith("_")
            and self.image.suffix != settings.PLACEHOLDER_SUFFIX
        )

    def _update_example(self):
        for line in self.example:
            if line and not line.isupper():
                return
        self.example = [line.lower() for line in self.example]

    @cached_property
    def styles(self):
        styles = []
        for path in self.directory.iterdir():
            if not path.stem[0] in {".", "_"} and path.stem not in {
                "config",
                settings.DEFAULT_STYLE,
            }:
                styles.append(path.stem)
        if styles or self.overlay != [Overlay()]:
            styles.append("default")
        styles.sort()
        return styles

    @cached_property
    def directory(self) -> Path:
        return self.datafile.path.parent

    @cached_property
    def image(self) -> Path:
        return self.get_image()

    def get_image(self, style: str = "") -> Path:
        style = style or settings.DEFAULT_STYLE

        if utils.urls.schema(style):
            url = style
            style = utils.text.fingerprint(url)

        self.directory.mkdir(exist_ok=True)
        for path in self.directory.iterdir():
            if path.stem == style and path.suffix != settings.PLACEHOLDER_SUFFIX:
                return path

        if style == settings.DEFAULT_STYLE:
            logger.debug(f"No default background image for template: {self.id}")
            return self.directory / (
                settings.DEFAULT_STYLE + settings.PLACEHOLDER_SUFFIX
            )

        logger.warning(f"Style {style!r} not available for template: {self.id}")
        return self.get_image()

    def jsonify(self, request: Request) -> dict:
        return {
            "id": self.id,
            "name": self.name,
            "lines": len(self.text),
            "overlays": len(self.overlay) if self.styles else 0,
            "styles": self.styles,
            "blank": request.app.url_for(
                "Memes.blank",
                template_id=self.id + "." + settings.DEFAULT_EXTENSION,
                _external=True,
                _scheme=settings.SCHEME,
            ),
            "example": {
                "text": self.example if any(self.example) else [],
                "url": self.build_example_url(request),
            },
            "source": self.source,
            "_self": self.build_self_url(request),
        }

    def build_self_url(self, request: Request) -> str:
        return request.app.url_for(
            "Templates.detail",
            id=self.id,
            _external=True,
            _scheme=settings.SCHEME,
        )

    def build_example_url(
        self,
        request: Request,
        *,
        extension: str = settings.DEFAULT_EXTENSION,
        external: bool = True,
    ) -> str:
        kwargs = {
            "template_id": self.id,
            "text_paths": utils.text.encode(self.example) + "." + extension,
            "_external": external,
        }
        if external:
            kwargs["_scheme"] = settings.SCHEME
        url = request.app.url_for("Memes.text", **kwargs)
        return utils.urls.clean(url)

    def build_custom_url(
        self,
        request: Request,
        text_lines: list[str],
        *,
        extension: str = settings.DEFAULT_EXTENSION,
        background: str = "",
        style: str = "",
        font: str = "",
    ):
        if extension not in settings.ALLOWED_EXTENSIONS:
            extension = settings.DEFAULT_EXTENSION
        if style == settings.DEFAULT_STYLE:
            style = ""
        url = request.app.url_for(
            "Memes.text",
            template_id="custom" if self.id == "_custom" else self.id,
            text_paths=utils.text.encode(text_lines) + "." + extension,
            _external=True,
            _scheme=settings.SCHEME,
            **utils.urls.params(background=background, style=style, font=font),
        )
        return utils.urls.clean(url)

    def build_path(
        self,
        text_lines: list[str],
        font_name: str,
        style: str,
        size: Dimensions,
        watermark: str,
        extension: str,
        frames: int = 0,
    ) -> Path:
        slug = utils.text.encode(text_lines)
        variant = str(self.text) + font_name + style + str(size) + watermark
        if frames:
            variant += str(frames)
        fingerprint = utils.text.fingerprint(variant, prefix="")
        filename = f"{slug}.{fingerprint}.{extension}"
        return Path(self.id) / filename

    @classmethod
    async def create(cls, url: str, *, force=False) -> "Template":
        try:
            parsed = furl(url)
        except ValueError as e:
            logger.error(e)
            return cls.objects.get("_error")

        if parsed.netloc and "memegen.link" in parsed.netloc:
            logger.info(f"Handling template URL: {url}")
            if len(parsed.path.segments) > 1:
                id = Path(parsed.path.segments[1]).stem
                if id != "custom":
                    return cls.objects.get_or_none(id) or cls.objects.get("_error")
                background = parsed.args.get("background")
                if not background:
                    return cls.objects.get("_error")
                url = background
                try:
                    parsed = furl(url)
                except ValueError as e:
                    logger.error(e)
                    return cls.objects.get("_error")

        id = utils.text.fingerprint(url)
        template = cls.objects.get_or_create(id, url)

        suffix = Path(str(parsed.path)).suffix
        if not suffix or len(suffix) > 10:
            logger.warning(f"Unable to determine image extension: {url}")
            suffix = settings.PLACEHOLDER_SUFFIX

        filename = "default" + suffix
        path = aiopath.AsyncPath(template.directory) / filename

        if await path.exists() and not settings.DEBUG and not force:
            logger.info(f"Found background {url} at {path}")
            return template

        logger.info(f"Saving background {url} to {path}")
        if not await utils.http.download(url, path):
            return template

        try:
            await asyncio.to_thread(utils.images.load, Path(path))
        except utils.images.EXCEPTIONS as e:
            logger.error(e)
            await path.unlink(missing_ok=True)

        return template

    async def check(self, style: str, *, force=False) -> bool:
        if style in {"", None, settings.DEFAULT_STYLE}:
            return True

        if style in self.styles:
            return True

        if not utils.urls.schema(style):
            logger.error(f"Invalid style for {self.id} template: {style}")
            return False

        filename = utils.text.fingerprint(style, suffix=self.image.suffix)
        path = aiopath.AsyncPath(self.directory) / filename
        if await path.exists() and not settings.DEBUG and not force:
            logger.info(f"Found overlay {style} at {path}")
            return True

        urls = style.split(",")
        logger.info(f"Embeding {len(urls)} overlay image(s) onto {path}")
        await asyncio.to_thread(shutil.copy, self.image, path)

        embedded = 0
        for index, url in enumerate(urls):
            success = await self._embed(index, url, path, force)
            if success:
                embedded += 1

        if len(urls) == 1 and not embedded:
            await path.unlink()

        return embedded == len(urls)

    async def _embed(
        self, index: int, url: str, background: aiopath.AsyncPath, force: bool
    ) -> bool:
        if url.strip() in {"", settings.DEFAULT_STYLE}:
            return True

        suffix = Path(str(furl(url).path)).suffix
        if not suffix:
            logger.warning(f"Unable to determine image extension: {url}")
            suffix = ".png"

        filename = utils.text.fingerprint(url, prefix="_embed-", suffix=suffix)
        foreground = aiopath.AsyncPath(self.directory) / filename

        if await foreground.exists() and not settings.DEBUG and not force:
            logger.info(f"Found overlay {url} at {foreground}")
        else:
            logger.info(f"Saving overlay {url} to {foreground}")
            await utils.http.download(url, foreground)

        try:
            await asyncio.to_thread(
                utils.images.embed, self, index, Path(foreground), Path(background)
            )
        except utils.images.EXCEPTIONS as e:
            logger.error(e)
            await foreground.unlink(missing_ok=True)

        return await foreground.exists()

    def clean(self):
        for path in self.directory.iterdir():
            if path.stem not in {"config", "default"}:
                path.unlink()

    def delete(self):
        if self.directory.exists():
            shutil.rmtree(self.directory)

    def matches(self, query: str) -> bool:
        example = " ".join(line.lower() for line in self.example)
        return any((query in self.id, query in self.name.lower(), query in example))

app/models/overlay.py METASEP
from dataclasses import dataclass

from ..types import Box, Dimensions


@dataclass
class Overlay:

    center_x: float = 0.5
    center_y: float = 0.5

    angle: float = 0.0

    scale: float = 0.25

    def get_size(self, background_size: Dimensions) -> Dimensions:
        background_width, background_height = background_size
        dimension = min(
            int(background_width * self.scale),
            int(background_height * self.scale),
        )
        return dimension, dimension

    def get_box(
        self, background_size: Dimensions, foreground_size: Dimensions | None = None
    ) -> Box:
        background_width, background_height = background_size
        if foreground_size is None:
            foreground_size = self.get_size(background_size)
        foreground_width, foreground_height = foreground_size
        box = (
            int(background_width * self.center_x - foreground_width / 2),
            int(background_height * self.center_y - foreground_height / 2),
            int(background_width * self.center_x + foreground_width / 2),
            int(background_height * self.center_y + foreground_height / 2),
        )
        return box

app/models/font.py METASEP
from __future__ import annotations

from dataclasses import KW_ONLY, asdict, dataclass
from pathlib import Path

from .. import settings


class Manager:
    @staticmethod
    def get(name: str) -> Font:
        name = name or settings.DEFAULT_FONT
        for font in FONTS:
            if name in (font.id, font.alias):
                return font
        raise ValueError(f"Unknown font: {name}")

    @staticmethod
    def all() -> list[Font]:
        return FONTS


@dataclass
class Font:

    filename: str
    id: str
    _: KW_ONLY
    alias: str = ""

    objects = Manager()

    @property
    def path(self) -> Path:
        return settings.ROOT / "fonts" / self.filename

    @property
    def data(self) -> dict:
        return asdict(self)


FONTS = [
    Font("TitilliumWeb-Black.ttf", "titilliumweb", alias="thick"),
    Font("NotoSans-Bold.ttf", "notosans"),
    Font("Kalam-Regular.ttf", "kalam", alias="comic"),
    Font("Impact.ttf", "impact"),
    Font("TitilliumWeb-SemiBold.ttf", "titilliumweb-thin", alias="thin"),
    Font("Segoe UI Bold.ttf", "segoe", alias="tiny"),
]

app/models/__init__.py METASEP
from .font import Font
from .overlay import Overlay
from .template import Template
from .text import Text

app/types.py METASEP
Box = tuple[int, int, int, int]
Dimensions = tuple[int, int]
Point = tuple[int, int]
Offset = tuple[int, int]

app/settings.py METASEP
import os
from pathlib import Path

ROOT = Path(__file__).parent.parent.resolve()

# Server configuration

PORT = int(os.environ.get("PORT", 5000))
WORKERS = int(os.environ.get("WEB_CONCURRENCY", 1))
DEBUG = bool(os.environ.get("DEBUG", False))

if "DOMAIN" in os.environ:  # staging / production
    SERVER_NAME = os.environ["DOMAIN"]
    RELEASE_STAGE = "staging" if "staging" in SERVER_NAME else "production"
    SCHEME = "https"
elif "HEROKU_APP_NAME" in os.environ:  # review apps
    SERVER_NAME = os.environ["HEROKU_APP_NAME"] + ".herokuapp.com"
    RELEASE_STAGE = "review"
    SCHEME = "https"
else:  # localhost
    SERVER_NAME = f"localhost:{PORT}"
    RELEASE_STAGE = "local"
    SCHEME = "http"

BASE_URL = f"{SCHEME}://{SERVER_NAME}"
DEPLOYED = RELEASE_STAGE != "local" and not DEBUG

# API

SUFFIX = " [DEBUG ONLY]" if not DEPLOYED else ""
PLACEHOLDER = "string"  # Swagger UI placeholder value

# Fonts

DEFAULT_FONT = "thick"

MINIMUM_FONT_SIZE = 7

# Image rendering

IMAGES_DIRECTORY = ROOT / "images"

DEFAULT_STYLE = "default"
DEFAULT_EXTENSION = "png"
ALLOWED_EXTENSIONS = [DEFAULT_EXTENSION, "jpg", "jpeg", "gif", "webp"]
PLACEHOLDER_SUFFIX = ".img"

PREVIEW_SIZE = (300, 300)
DEFAULT_SIZE = (600, 600)

MAXIMUM_PIXELS = 1920 * 1080
MAXIMUM_FRAMES = 20

# Watermarks

DISABLED_WATERMARK = "none"
DEFAULT_WATERMARK = "Memegen.link"
ALLOWED_WATERMARKS = [DEFAULT_WATERMARK]

WATERMARK_HEIGHT = 15

PREVIEW_TEXT = "PREVIEW"

# Test images

TEST_IMAGES_DIRECTORY = ROOT / "app" / "tests" / "images"
TEST_IMAGES = [
    (
        "iw",
        ["tests code", "in production"],
        "jpg",
    ),
    (
        "fry",
        ["a", "b"],
        "png",
    ),
    (
        "fry",
        ["short line", "longer line of text than the short one"],
        "png",
    ),
    (
        "fry",
        ["longer line of text than the short one", "short line"],
        "png",
    ),
    (
        "sparta",
        ["", "this is a wide image!"],
        "png",
    ),
    (
        "ski",
        [
            "if you try to put a bunch more text than can possibly fit on a meme",
            "you're gonna have a bad time",
        ],
        "png",
    ),
    (
        "ds",
        ["Push this button.", "Push that button.", "can't decide which is worse"],
        "png",
    ),
    (
        "spongebob",
        ["You: Stop talking like that", "Me: Stop talking like that"],
        "png",
    ),
    (
        "mouth",
        ["Sales Team presenting solution that won't work", "Excited Customer", "Me"],
        "png",
    ),
    (
        "cmm",
        ["Many\nextra\nlines\nof\ntext"],
        "png",
    ),
    (
        "oprah",
        ["you get animated text", "and you get animated text"],
        "gif",
    ),
]

# Analytics

TRACK_REQUESTS = True
REMOTE_TRACKING_URL = os.getenv("REMOTE_TRACKING_URL")

REMOTE_TRACKING_ERRORS = 0
REMOTE_TRACKING_ERRORS_LIMIT = int(os.getenv("REMOTE_TRACKING_ERRORS_LIMIT", "10"))

BUGSNAG_API_KEY = os.getenv("BUGSNAG_API_KEY")

app/main.py METASEP
import asyncio
import random

import log
from sanic import Sanic, response
from sanic_ext import openapi

from app import config, helpers, settings, utils

app = Sanic(name="memegen")
config.init(app)


@app.get("/")
@openapi.exclude(True)
def index(request):
    return response.redirect("/docs")


@app.get("/examples")
@openapi.exclude(True)
async def examples(request):
    animated = utils.urls.flag(request, "animated")
    items = await asyncio.to_thread(helpers.get_example_images, request, "", animated)
    urls = [items[0] for items in items]
    if settings.DEBUG:
        refresh = int(request.args.get("refresh", 5 * 60))
    else:
        refresh = 0
        random.shuffle(urls)
    content = utils.html.gallery(urls, columns=True, refresh=refresh)
    return response.html(content)


@app.get("/test")
@openapi.exclude(True)
async def test(request):
    if not settings.DEBUG:
        return response.redirect("/")

    urls = await asyncio.to_thread(helpers.get_test_images, request)
    content = utils.html.gallery(urls, columns=False, refresh=20)
    return response.html(content)


@app.get("/favicon.ico")
@openapi.exclude(True)
async def favicon(request):
    return await response.file("app/static/favicon.ico")


@app.get("/robots.txt")
@openapi.exclude(True)
async def robots(request):
    return await response.file("app/static/robots.txt")


if __name__ == "__main__":
    log.reset()
    log.silence("datafiles", allow_warning=True)
    app.run(
        host="0.0.0.0",
        port=settings.PORT,
        workers=settings.WORKERS,
        debug=settings.DEBUG,
        access_log=False,
        motd=False,
    )

app/helpers.py METASEP
from sanic import Request

from . import settings, utils
from .models import Template


def get_valid_templates(
    request: Request, query: str = "", animated: bool | None = None
) -> list[dict]:
    templates = Template.objects.filter(valid=True, _exclude="_custom")
    if query:
        templates = [t for t in templates if t.matches(query)]
    else:
        templates = sorted(templates)
    if animated is True:
        templates = [t for t in templates if "animated" in t.styles]
    elif animated is False:
        templates = [t for t in templates if "animated" not in t.styles]
    return [template.jsonify(request) for template in templates]


def get_example_images(
    request: Request, query: str = "", animated: bool | None = None
) -> list[tuple[str, str]]:
    templates = Template.objects.filter(valid=True, _exclude="_custom")
    if query:
        templates = [t for t in templates if t.matches(query)]
    else:
        templates = sorted(templates)

    images = []
    for template in templates:

        if animated is True and "animated" not in template.styles:
            continue

        if "animated" in template.styles and animated is not False:
            extension = "gif"
        else:
            extension = settings.DEFAULT_EXTENSION

        example = template.build_example_url(request, extension=extension)
        self = template.build_self_url(request)
        images.append((example, self))

    return images


def get_test_images(request: Request) -> list[str]:
    return [
        request.app.url_for(
            "Memes.text",
            template_id=id,
            text_paths=utils.text.encode(lines) + "." + extension,
        )
        for id, lines, extension in settings.TEST_IMAGES
    ]

app/config.py METASEP
import bugsnag
from aiohttp.client_exceptions import ClientPayloadError
from PIL import UnidentifiedImageError
from sanic.exceptions import MethodNotSupported, NotFound
from sanic.handlers import ErrorHandler

from . import settings, utils, views

IGNORED_EXCEPTIONS = (
    ClientPayloadError,
    MethodNotSupported,
    NotFound,
    UnidentifiedImageError,
)


class BugsnagErrorHandler(ErrorHandler):
    def default(self, request, exception):
        if self._should_notify(exception):
            bugsnag.notify(exception, metadata={"request": request.url})
        return super().default(request, exception)

    @staticmethod
    def _should_notify(exception) -> bool:
        if not settings.BUGSNAG_API_KEY:
            return False
        if isinstance(exception, IGNORED_EXCEPTIONS):
            return False
        return True


def init(app):
    app.config.API_HOST = app.config.SERVER_NAME = settings.SERVER_NAME
    app.config.API_SCHEMES = [settings.SCHEME]
    app.config.API_VERSION = utils.meta.version()
    app.config.API_TITLE = "Memegen.link"
    app.config.API_CONTACT_EMAIL = "support@maketested.com"
    app.config.API_LICENSE_NAME = "View the license"
    app.config.API_LICENSE_URL = (
        "https://github.com/jacebrowning/memegen/blob/main/LICENSE.txt"
    )

    app.config.OAS_UI_DEFAULT = "swagger"
    app.config.OAS_UI_REDOC = False
    app.config.SWAGGER_UI_CONFIGURATION = {
        "operationsSorter": "alpha",
        "docExpansion": "list",
    }

    app.ext.openapi.add_security_scheme("ApiKeyAuth", type="apiKey", name="X-API-KEY")
    app.ext.openapi.secured("ApiKeyAuth")

    app.blueprint(views.clients.blueprint)
    app.blueprint(views.memes.blueprint)
    app.blueprint(views.templates.blueprint)
    app.blueprint(views.shortcuts.blueprint)  # registered last to avoid collisions

    app.error_handler = BugsnagErrorHandler()
    bugsnag.configure(
        api_key=settings.BUGSNAG_API_KEY,
        project_root="/app",
        release_stage=settings.RELEASE_STAGE,
    )

app/__init__.py METASEP

app/views/templates.py METASEP
import asyncio
from contextlib import suppress
from dataclasses import dataclass

from sanic import Blueprint, exceptions, response
from sanic_ext import openapi

from .. import helpers, settings, utils
from ..models import Template

blueprint = Blueprint("Templates", url_prefix="/templates")


@dataclass
class ExampleResponse:
    text: list[str]
    url: str


@dataclass
class TemplateResponse:
    id: str
    name: str
    lines: int
    overlays: int
    styles: list[str]
    blank: str
    example: ExampleResponse
    source: str
    _self: str


@blueprint.get("/")
@openapi.summary("List all templates")
@openapi.parameter(
    "animated",
    bool,
    "query",
    description="Limit results to templates supporting animation",
)
@openapi.parameter(
    "filter", str, "query", description="Part of the name or example to match"
)
@openapi.response(
    200,
    {"application/json": list[TemplateResponse]},
    "Successfully returned a list of all templates",
)
async def index(request):
    query = request.args.get("filter", "").lower()
    animated = utils.urls.flag(request, "animated")
    data = await asyncio.to_thread(
        helpers.get_valid_templates, request, query, animated
    )
    return response.json(data)


@blueprint.get("/<id:slug>")
@openapi.summary("View a specific template")
@openapi.parameter("id", str, "path")
@openapi.response(
    200,
    {"application/json": TemplateResponse},
    "Successfully returned a specific templates",
)
@openapi.response(404, str, description="Template not found")
async def detail(request, id):
    template: Template = Template.objects.get_or_none(id)
    if template:
        return response.json(template.jsonify(request))
    raise exceptions.NotFound(f"Template not found: {id}")


@dataclass
class MemeRequest:
    text_lines: list[str]
    extension: str
    redirect: bool


@dataclass
class MemeResponse:
    url: str


@blueprint.post("/<id:slug>")
@openapi.tag("Memes")
@openapi.operation("Memes.create_from_template")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Create a meme from a template" + settings.SUFFIX)
@openapi.parameter("id", str, "path")
@openapi.body({"application/json": MemeRequest})
@openapi.response(
    201,
    {"application/json": MemeResponse},
    "Successfully created a meme from a template",
)
async def build(request, id):
    return await generate_url(request, id)


@dataclass
class CustomRequest:
    background: str
    style: str
    text_lines: list[str]
    font: str
    extension: str
    redirect: bool


@blueprint.post("/custom")
@openapi.tag("Memes")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Create a meme from any image" + settings.SUFFIX)
@openapi.body({"application/json": CustomRequest})
@openapi.response(
    201,
    {"application/json": MemeResponse},
    "Successfully created a meme from a custom image",
)
async def custom(request):
    return await generate_url(request)


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: Template = Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)

app/views/shortcuts.py METASEP
from sanic import Blueprint, exceptions, response
from sanic.log import logger
from sanic_ext import openapi

from .. import models, settings, utils

blueprint = Blueprint("Shortcuts", url_prefix="/")


@blueprint.get(r"/images/<template_id:[^.]+>")
@openapi.summary("Redirect to an example image")
@openapi.parameter("template_id", str, "path")
@openapi.response(
    302, {"image/*": bytes}, "Successfully redirected to an example image"
)
@openapi.response(404, {"text/html": str}, "Template not found")
@openapi.response(501, {"text/html": str}, "Template not fully implemented")
async def example_path(request, template_id):
    template_id = utils.urls.clean(template_id)

    if settings.DEBUG:
        template = models.Template.objects.get_or_create(template_id)
    else:
        template = models.Template.objects.get_or_none(template_id)

    if template and template.valid:
        url = template.build_example_url(request, external=False)
        if settings.DEBUG:
            url = url.removesuffix(".png")
        return response.redirect(url)

    if settings.DEBUG:
        if "<" in template_id:
            message = f"Replace {template_id!r} in the URL"
        else:
            message = f"Template not fully implemented: {template}"
            logger.warning(message)
            template.datafile.save()
        raise exceptions.SanicException(message, 501)

    raise exceptions.NotFound(f"Template not found: {template_id}")


@blueprint.get(r"/<template_id:.+\.\w+>")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Redirect to an example image" + settings.SUFFIX)
@openapi.parameter("template_id", str, "path")
@openapi.response(
    302, {"image/*": bytes}, "Successfully redirected to an example image"
)
@openapi.response(404, {"text/html": str}, "Template not found")
async def legacy_example_image(request, template_id):
    template_id, extension = template_id.rsplit(".", 1)
    template = models.Template.objects.get_or_none(template_id)
    if template:
        url = template.build_example_url(request, extension=extension, external=False)
        return response.redirect(url)
    raise exceptions.NotFound(f"Template not found: {template_id}")


@blueprint.get(r"/<template_id:slug>")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Redirect to an example image" + settings.SUFFIX)
@openapi.parameter("template_id", str, "path")
@openapi.response(
    302, {"image/*": bytes}, "Successfully redirected to an example image"
)
async def legacy_example_path(request, template_id):
    template_id = template_id.strip("/")
    return response.redirect(f"/images/{template_id}")


@blueprint.get(r"/images/<template_id:slug>/<text_paths:[^/].*>")
@openapi.summary("Redirect to a custom image")
@openapi.parameter("text_paths", str, "path")
@openapi.parameter("template_id", str, "path")
@openapi.response(302, {"image/*": bytes}, "Successfully redirected to a custom image")
async def custom_path(request, template_id, text_paths):
    if template_id == "images":
        return response.redirect(f"/images/{text_paths}".removesuffix("/"))

    if not settings.DEBUG:
        url = request.app.url_for(
            "Memes.text",
            template_id=template_id,
            text_paths=utils.urls.clean(text_paths) + "." + settings.DEFAULT_EXTENSION,
        )
        return response.redirect(url)

    template = models.Template.objects.get_or_create(template_id)
    template.datafile.save()
    animated = utils.urls.flag(request, "animated")
    extension = "gif" if animated else "png"
    content = utils.html.gallery(
        [f"/images/{template_id}/{text_paths}.{extension}"],
        columns=False,
        refresh=30 if animated else 3,
        query_string=request.query_string,
    )
    return response.html(content)


@blueprint.get(r"/<template_id:(?!templates)[a-z-]+>/<text_paths:[^/].*\.\w+>")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Redirect to a custom image" + settings.SUFFIX)
@openapi.parameter("text_paths", str, "path")
@openapi.parameter("template_id", str, "path")
@openapi.response(302, {"image/*": bytes}, "Successfully redirected to a custom image")
@openapi.response(404, {"text/html": str}, description="Template not found")
async def legacy_custom_image(request, template_id, text_paths):
    text_paths, extension = text_paths.rsplit(".", 1)
    template = models.Template.objects.get_or_none(template_id)
    if template:
        url = request.app.url_for(
            "Memes.text",
            template_id=template_id,
            text_paths=text_paths + "." + extension,
        )
        return response.redirect(url)
    raise exceptions.NotFound(f"Template not found: {template_id}")


@blueprint.get(r"/<template_id:(?!templates)[a-z-]+>/<text_paths:[^/].*>")
@openapi.exclude(settings.DEPLOYED)
@openapi.summary("Redirect to a custom image" + settings.SUFFIX)
@openapi.parameter("text_paths", str, "path")
@openapi.parameter("template_id", str, "path")
@openapi.response(302, {"image/*": bytes}, "Successfully redirected to a custom image")
async def legacy_custom_path(request, template_id, text_paths):
    if template_id == "images":
        return response.redirect(f"/images/{text_paths}".removesuffix("/"))
    return response.redirect(f"/images/{template_id}/{text_paths}")

app/views/memes.py METASEP
import asyncio
from dataclasses import dataclass

from sanic import Blueprint, exceptions, response
from sanic.log import logger
from sanic_ext import openapi

from .. import helpers, models, settings, utils
from .templates import generate_url

blueprint = Blueprint("Memes", url_prefix="/images")


@dataclass
class ExampleResponse:
    url: str
    template: str


@blueprint.get("/")
@openapi.summary("List example memes")
@openapi.operation("Memes.list")
@openapi.parameter(
    "filter", str, "query", description="Part of the template name or example to match"
)
@openapi.response(
    200,
    {"application/json": list[ExampleResponse]},
    "Successfully returned a list of example memes",
)
async def index(request):
    query = request.args.get("filter", "").lower()
    examples = await asyncio.to_thread(helpers.get_example_images, request, query)
    return response.json(
        [{"url": url, "template": template} for url, template in examples]
    )


@dataclass
class MemeRequest:
    template_id: str
    style: list[str]
    text_lines: list[str]
    font: str
    extension: str
    redirect: bool


@dataclass
class MemeResponse:
    url: str


@dataclass
class ErrorResponse:
    error: str


@blueprint.post("/")
@openapi.summary("Create a meme from a template")
@openapi.operation("Memes.create")
@openapi.body({"application/json": MemeRequest})
@openapi.response(
    201, {"application/json": MemeResponse}, "Successfully created a meme"
)
@openapi.response(
    400,
    {"application/json": ErrorResponse},
    'Required "template_id" missing in request body',
)
@openapi.response(
    404, {"application/json": ErrorResponse}, 'Specified "template_id" does not exist'
)
async def create(request):
    return await generate_url(request, template_id_required=True)


@dataclass
class AutomaticRequest:
    text: str
    safe: bool
    redirect: bool


@blueprint.post("/automatic")
@openapi.exclude(not settings.REMOTE_TRACKING_URL)
@openapi.summary("Create a meme from word or phrase")
@openapi.body({"application/json": AutomaticRequest})
@openapi.response(
    201, {"application/json": MemeResponse}, "Successfully created a meme"
)
@openapi.response(
    400, {"application/json": ErrorResponse}, 'Required "text" missing in request body'
)
async def automatic(request):
    if request.form:
        payload = dict(request.form)
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    try:
        query = payload["text"]
    except KeyError:
        return response.json({"error": '"text" is required'}, status=400)

    results = await utils.meta.search(request, query, payload.get("safe", True))
    logger.info(f"Found {len(results)} result(s)")
    if not results:
        return response.json({"message": f"No results matched: {query}"}, status=404)

    url = utils.urls.normalize(results[0]["image_url"])
    confidence = results[0]["confidence"]
    logger.info(f"Top result: {url} ({confidence})")
    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url, "confidence": confidence}, status=201)


@dataclass
class CustomRequest:
    background: str
    style: str
    text_lines: list[str]
    font: str
    extension: str
    redirect: bool


@blueprint.post("/custom")
@openapi.summary("Create a meme from any image")
@openapi.body({"application/json": CustomRequest})
@openapi.response(
    201,
    {"application/json": MemeResponse},
    description="Successfully created a meme from a custom image",
)
async def custom(request):
    return await generate_url(request)


@blueprint.get("/custom")
@openapi.summary("List popular custom memes")
@openapi.operation("Memes.list_custom")
@openapi.parameter("safe", bool, "query", description="Exclude NSFW results")
@openapi.parameter(
    "filter", str, "query", description="Part of the meme's text to match"
)
@openapi.response(
    200,
    {"application/json": list[MemeResponse]},
    "Successfully returned a list of custom memes",
)
async def list_custom(request):
    query = request.args.get("filter", "").lower()
    safe = utils.urls.flag(request, "safe", True)

    results = await utils.meta.search(request, query, safe, mode="results")
    logger.info(f"Found {len(results)} result(s)")
    if not results:
        return response.json({"message": f"No results matched: {query}"}, status=404)

    items = []
    for result in results:
        url = utils.urls.normalize(result["image_url"])
        url, _updated = await utils.meta.tokenize(request, url)
        items.append({"url": url})

    return response.json(items, status=200)


@blueprint.get(r"/<template_id:.+\.\w+>")
@openapi.tag("Templates")
@openapi.summary("Display a template background")
@openapi.parameter("template_id", str, "path")
@openapi.response(
    200, {"image/*": bytes}, "Successfully displayed a template background"
)
@openapi.response(404, {"image/*": bytes}, "Template not found")
@openapi.response(415, {"image/*": bytes}, "Unable to download image URL")
@openapi.response(
    422,
    {"image/*": bytes},
    "Invalid style for template or no image URL specified for custom template",
)
async def blank(request, template_id):
    template_id, extension = template_id.rsplit(".", 1)

    if request.args.get("style") == "animated" and extension != "gif":
        # TODO: Move this pattern to utils
        params = {k: v for k, v in request.args.items() if k != "style"}
        url = request.app.url_for(
            "Memes.blank",
            template_id=template_id + ".gif",
            **params,
        )
        return response.redirect(utils.urls.clean(url), status=301)

    return await render_image(request, template_id, extension=extension)


@blueprint.get(r"/<template_id:slug>/<text_paths:[^/].*\.\w+>")
@openapi.summary("Display a custom meme")
@openapi.parameter("text_paths", str, "path")
@openapi.parameter("template_id", str, "path")
@openapi.response(200, {"image/*": bytes}, "Successfully displayed a custom meme")
@openapi.response(404, {"image/*": bytes}, "Template not found")
@openapi.response(414, {"image/*": bytes}, "Custom text too long (length >200)")
@openapi.response(415, {"image/*": bytes}, "Unable to download image URL")
@openapi.response(
    422,
    {"image/*": bytes},
    "Invalid style for template or no image URL specified for custom template",
)
async def text(request, template_id, text_paths):
    text_paths, extension = text_paths.rsplit(".", 1)

    if request.args.get("style") == "animated" and extension != "gif":
        # TODO: Move this pattern to utils
        params = {k: v for k, v in request.args.items() if k != "style"}
        url = request.app.url_for(
            "Memes.text",
            template_id=template_id,
            text_paths=text_paths + ".gif",
            **params,
        )
        return response.redirect(utils.urls.clean(url), status=301)

    slug, updated = utils.text.normalize(text_paths)
    if updated:
        url = request.app.url_for(
            "Memes.text",
            template_id=template_id,
            text_paths=slug + "." + extension,
            **request.args,
        )
        return response.redirect(utils.urls.clean(url), status=301)

    url, updated = await utils.meta.tokenize(request, request.url)
    if updated:
        return response.redirect(url, status=302)

    watermark, updated = await utils.meta.get_watermark(request)
    if updated:
        # TODO: Move this pattern to utils
        params = {k: v for k, v in request.args.items() if k != "watermark"}
        url = request.app.url_for(
            "Memes.text",
            template_id=template_id,
            text_paths=slug + "." + extension,
            **params,
        )
        return response.redirect(utils.urls.clean(url), status=302)

    return await render_image(request, template_id, slug, watermark, extension)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

    frames = int(request.args.get("frames", 0))

    path = await asyncio.to_thread(
        utils.images.save,
        template,
        lines,
        watermark,
        font_name=font_name,
        extension=extension,
        style=style,
        size=size,
        maximum_frames=frames,
    )
    return await response.file(path, status)

app/views/clients.py METASEP
import asyncio
from dataclasses import dataclass
from datetime import datetime

from sanic import Blueprint, response
from sanic.log import logger
from sanic_ext import openapi

from .. import models, utils

blueprint = Blueprint("Clients", url_prefix="/")


@dataclass
class AuthResponse:
    email: str
    image_access: bool
    search_access: bool
    created: datetime
    modified: datetime


@dataclass
class ErrorResponse:
    error: str


@blueprint.post("/auth")
@openapi.summary("Validate your API key")
@openapi.response(200, {"application/json": AuthResponse}, "Your API key is valid")
@openapi.response(401, {"application/json": ErrorResponse}, "Your API key is invalid")
async def validate(request):
    info = await utils.meta.authenticate(request)
    return response.json(
        info or {"error": "API key missing or invalid."},
        status=200 if info else 401,
    )


@dataclass
class FontResponse:
    filename: str
    id: str
    alias: str


@blueprint.get("/fonts")
@openapi.summary("List available fonts")
@openapi.response(
    200,
    {"application/json": list[FontResponse]},
    "Successfully returned a list of fonts",
)
async def fonts(request):
    return response.json([font.data for font in models.Font.objects.all()])


@blueprint.get("/images/preview.jpg")
@openapi.summary("Display a preview of a custom meme")
@openapi.parameter("lines[]", str, "query", description="Lines of text to render")
@openapi.parameter("style", str, "query", description="Style name or custom overlay")
@openapi.parameter(
    "template", str, "query", description="Template ID, URL, or custom background"
)
@openapi.response(200, {"image/jpeg": bytes}, "Successfully displayed a custom meme")
async def preview(request):
    id = request.args.get("template", "_error")
    lines = request.args.getlist("lines[]", [])
    style = request.args.get("style") or ",".join(request.args.getlist("styles[]", []))
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    return await preview_image(request, id, lines, style)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)

app/views/__init__.py METASEP
from . import clients, memes, shortcuts, templates

app/views/helpers.py METASEP

import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


infile
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


infile
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

    frames = int(request.args.get("frames", 0))

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

    frames = int(request.args.get("frames", 0))

    path = await asyncio.to_thread(
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

    frames = int(request.args.get("frames", 0))

    path = await asyncio.to_thread(
        utils.images.save,
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

    frames = int(request.args.get("frames", 0))

    path = await asyncio.to_thread(
        utils.images.save,
        template,
        lines,
        watermark,
        font_name=font_name,
        extension=extension,
        style=style,
        size=size,
        maximum_frames=frames,
    )
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

inproject
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

common
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
        size = int(request.args.get("width", 0)), int(request.args.get("height", 0))
        if 0 < size[0] < 10 or 0 < size[1] < 10:
            raise ValueError(f"dimensions are too small: {size}")
    except ValueError as e:
        logger.error(f"Invalid size: {e}")
        size = 0, 0
        status = 422

common
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
            style = settings.DEFAULT_STYLE
            status = 422

    else:
        template = models.Template.objects.get_or_none(id)
        if not template or not template.image.exists():
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            if id != settings.PLACEHOLDER:
                status = 404

        style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")
        if not await template.check(style):
            if utils.urls.schema(style):
                status = 415
            elif style != settings.PLACEHOLDER:
                status = 422

    if extension not in settings.ALLOWED_EXTENSIONS:
        extension = settings.DEFAULT_EXTENSION
        status = 422

    font_name = utils.urls.arg(request.args, "", "font")
    if font_name == settings.PLACEHOLDER:
        font_name = ""
    else:
        try:
            models.Font.objects.get(font_name)
        except ValueError:
            font_name = ""
            status = 422

    try:
common
import asyncio
from contextlib import suppress

non_informative
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)
non_informative
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
random
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
random
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
random
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
    else:
        try:
            payload = request.json or {}
        except exceptions.InvalidUsage:
            payload = {}

    with suppress(KeyError):
        payload["style"] = payload.pop("style[]")
    with suppress(KeyError):
        payload["text_lines"] = payload.pop("text_lines[]")

    if template_id_required:
        try:
            template_id = payload["template_id"]
        except KeyError:
            return response.json({"error": '"template_id" is required'}, status=400)
        else:
            template_id = utils.text.slugify(template_id)

    style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")
    if isinstance(style, list):
        style = ",".join([(s.strip() or "default") for s in style])
    while style.endswith(",default"):
        style = style.removesuffix(",default")
    text_lines = utils.urls.arg(payload, [], "text_lines")
    font = utils.urls.arg(payload, "", "font")
    background = utils.urls.arg(payload, "", "background", "image_url")
    extension = utils.urls.arg(payload, "", "extension")

    if style == "animated":
        extension = "gif"
        style = ""

    status = 201

    if template_id:
        template: models.Template = models.Template.objects.get_or_create(template_id)
        url = template.build_custom_url(
            request,
            text_lines,
            style=style,
            font=font,
            extension=extension,
        )
        if not template.valid:
            status = 404
            template.delete()
    else:
        template = models.Template("_custom")
        url = template.build_custom_url(
            request,
            text_lines,
            background=background,
            style=style,
            font=font,
            extension=extension,
        )

    url, _updated = await utils.meta.tokenize(request, url)

    if payload.get("redirect", False):
        return response.redirect(utils.urls.add(url, status="201"))

    return response.json({"url": url}, status=status)


async def preview_image(request, id: str, lines: list[str], style: str):
    error = ""

    id = utils.urls.clean(id)
    if utils.urls.schema(id):
        template = await models.Template.create(id)
        if not template.image.exists():
            logger.error(f"Unable to download image URL: {id}")
            template = models.Template.objects.get("_error")
            error = "Invalid Background"
    else:
        template = models.Template.objects.get_or_none(id)
        if not template:
            logger.error(f"No such template: {id}")
            template = models.Template.objects.get("_error")
            error = "Unknown Template"

    if not any(line.strip() for line in lines):
        lines = template.example

    if not utils.urls.schema(style):
        style = style.strip().lower()
    if not await template.check(style):
        error = "Invalid Overlay"

    data, content_type = await asyncio.to_thread(
        utils.images.preview, template, lines, style=style, watermark=error.upper()
    )
    return response.raw(data, content_type=content_type)


async def render_image(
    request,
    id: str,
    slug: str = "",
    watermark: str = "",
    extension: str = settings.DEFAULT_EXTENSION,
):
    lines = utils.text.decode(slug)
    asyncio.create_task(utils.meta.track(request, lines))

    status = int(utils.urls.arg(request.args, "200", "status"))

    if any(len(part.encode()) > 200 for part in slug.split("/")):
        logger.error(f"Slug too long: {slug}")
        slug = slug[:50] + "..."
        lines = utils.text.decode(slug)
        template = models.Template.objects.get("_error")
        style = settings.DEFAULT_STYLE
        status = 414

    elif id == "custom":
        url = utils.urls.arg(request.args, None, "background", "alt")
        if url:
            template = await models.Template.create(url)
            if not template.image.exists():
                logger.error(f"Unable to download image URL: {url}")
                template = models.Template.objects.get("_error")
                if url != settings.PLACEHOLDER:
                    status = 415

            style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")
            if not utils.urls.schema(style):
                style = style.lower()
            if not await template.check(style):
                if utils.urls.schema(style):
                    status = 415
                elif style != settings.PLACEHOLDER:
                    status = 422

        else:
            logger.error("No image URL specified for custom template")
            template = models.Template.objects.get("_error")
random
import asyncio
from contextlib import suppress

from sanic import exceptions, response
from sanic.log import logger

from .. import models, settings, utils


async def generate_url(
    request, template_id: str = "", *, template_id_required: bool = False
):
    if request.form:
        payload = dict(request.form)
        for key in list(payload.keys()):
            if "lines" not in key and "style" not in key:
                payload[key] = payload.pop(key)[0]
random
async def render_image( async def preview_image(request, id: str, lines: list[str], style: str):         lines = utils.text.decode(slug)             template = models.Template.objects.get("_error")         url = utils.urls.arg(request.args, None, "background", "alt")         if url:             template = await models.Template.create(url)             if not template.image.exists():                 logger.error(f"Unable to download image URL: {url}")                 if url != settings.PLACEHOLDER:             style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style")     if not utils.urls.schema(style):             if not await template.check(style):             if utils.urls.schema(style):         template = models.Template.objects.get_or_none(id)         if not template or not template.image.exists():             template_id = utils.text.slugify(template_id)     style: str = utils.urls.arg(payload, "", "style", "overlay", "alt")         style = utils.urls.arg(request.args, settings.DEFAULT_STYLE, "style", "alt")     text_lines = utils.urls.arg(payload, [], "text_lines")     font = utils.urls.arg(payload, "", "font")     background = utils.urls.arg(payload, "", "background", "image_url")     extension = utils.urls.arg(payload, "", "extension")     font_name = utils.urls.arg(request.args, "", "font")         template: models.Template = models.Template.objects.get_or_create(template_id)         url = template.build_custom_url(             models.Font.objects.get(font_name)         if not template.valid:             template.delete()         template = models.Template("_custom")     path = await asyncio.to_thread(         utils.images.save,         template,     url, _updated = await utils.meta.tokenize(request, url)         return response.redirect(utils.urls.add(url, status="201"))     return response.json({"url": url}, status=status)     return await response.file(path, status)     id = utils.urls.clean(id)     if utils.urls.schema(id):         template = await models.Template.create(id)         if not template:         lines = template.example     data, content_type = await asyncio.to_thread(         utils.images.preview, template, lines, style=style, watermark=error.upper()     return response.raw(data, content_type=content_type)     asyncio.create_task(utils.meta.track(request, lines))     status = int(utils.urls.arg(request.args, "200", "status"))     if any(len(part.encode()) > 200 for part in slug.split("/")):     if payload.get("redirect", False):     frames = int(request.args.get("frames", 0))         size = int(request.args.get("width", 0)), int(request.args.get("height", 0)) from sanic import exceptions, response     error = ""                     status = 415         error = "Invalid Overlay"             style = settings.DEFAULT_STYLE     else: