├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_P.rst ├── coo.png ├── coo ├── __init__.py ├── _exceptions.py ├── _utils.py └── coo.py ├── docs ├── Makefile ├── make.bat └── source │ ├── changelog.rst │ ├── conf.py │ ├── index.rst │ ├── list_schedule.rst │ ├── schedule.rst │ └── twitter_api.rst ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_coo.py └── test_utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = coo 4 | 5 | [report] 6 | omit = 7 | tests/* 8 | setup.py 9 | exclude_lines = 10 | def __repr__ 11 | def __str__ 12 | if settings.DEBUG 13 | raise AssertionError 14 | raise NotImplementedError 15 | if __name__ == .__main__.: 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/vim,osx,linux,macos,python,windows,pycharm,sublimetext,visualstudio,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### OSX ### 48 | # General 49 | 50 | # Icon must end with two \r 51 | 52 | # Thumbnails 53 | 54 | # Files that might appear in the root of a volume 55 | 56 | # Directories potentially created on remote AFP share 57 | 58 | ### PyCharm ### 59 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 60 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 61 | 62 | # User-specific stuff 63 | .idea/**/workspace.xml 64 | .idea/**/tasks.xml 65 | .idea/**/usage.statistics.xml 66 | .idea/**/dictionaries 67 | .idea/**/shelf 68 | 69 | # Generated files 70 | .idea/**/contentModel.xml 71 | 72 | # Sensitive or high-churn files 73 | .idea/**/dataSources/ 74 | .idea/**/dataSources.ids 75 | .idea/**/dataSources.local.xml 76 | .idea/**/sqlDataSources.xml 77 | .idea/**/dynamic.xml 78 | .idea/**/uiDesigner.xml 79 | .idea/**/dbnavigator.xml 80 | 81 | # Gradle 82 | .idea/**/gradle.xml 83 | .idea/**/libraries 84 | 85 | # Gradle and Maven with auto-import 86 | # When using Gradle or Maven with auto-import, you should exclude module files, 87 | # since they will be recreated, and may cause churn. Uncomment if using 88 | # auto-import. 89 | # .idea/modules.xml 90 | # .idea/*.iml 91 | # .idea/modules 92 | 93 | # CMake 94 | cmake-build-*/ 95 | 96 | # Mongo Explorer plugin 97 | .idea/**/mongoSettings.xml 98 | 99 | # File-based project format 100 | *.iws 101 | 102 | # IntelliJ 103 | out/ 104 | 105 | # mpeltonen/sbt-idea plugin 106 | .idea_modules/ 107 | 108 | # JIRA plugin 109 | atlassian-ide-plugin.xml 110 | 111 | # Cursive Clojure plugin 112 | .idea/replstate.xml 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | # Editor-based Rest Client 121 | .idea/httpRequests 122 | 123 | ### PyCharm Patch ### 124 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 125 | 126 | # *.iml 127 | # modules.xml 128 | # .idea/misc.xml 129 | # *.ipr 130 | 131 | # Sonarlint plugin 132 | .idea/sonarlint 133 | 134 | ### Python ### 135 | # Byte-compiled / optimized / DLL files 136 | __pycache__/ 137 | *.py[cod] 138 | *$py.class 139 | 140 | # C extensions 141 | *.so 142 | 143 | # Distribution / packaging 144 | .Python 145 | build/ 146 | develop-eggs/ 147 | dist/ 148 | downloads/ 149 | eggs/ 150 | .eggs/ 151 | lib/ 152 | lib64/ 153 | parts/ 154 | sdist/ 155 | var/ 156 | wheels/ 157 | *.egg-info/ 158 | .installed.cfg 159 | *.egg 160 | MANIFEST 161 | 162 | # PyInstaller 163 | # Usually these files are written by a python script from a template 164 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 165 | *.manifest 166 | *.spec 167 | 168 | # Installer logs 169 | pip-log.txt 170 | pip-delete-this-directory.txt 171 | 172 | # Unit test / coverage reports 173 | htmlcov/ 174 | .tox/ 175 | .coverage 176 | .coverage.* 177 | .cache 178 | nosetests.xml 179 | coverage.xml 180 | *.cover 181 | .hypothesis/ 182 | .pytest_cache/ 183 | 184 | # Translations 185 | *.mo 186 | *.pot 187 | 188 | # Django stuff: 189 | *.log 190 | local_settings.py 191 | db.sqlite3 192 | 193 | # Flask stuff: 194 | instance/ 195 | .webassets-cache 196 | 197 | # Scrapy stuff: 198 | .scrapy 199 | 200 | # Sphinx documentation 201 | docs/_build/ 202 | 203 | # PyBuilder 204 | target/ 205 | 206 | # Jupyter Notebook 207 | .ipynb_checkpoints 208 | 209 | # IPython 210 | profile_default/ 211 | ipython_config.py 212 | 213 | # pyenv 214 | .python-version 215 | 216 | # celery beat schedule file 217 | celerybeat-schedule 218 | 219 | # SageMath parsed files 220 | *.sage.py 221 | 222 | # Environments 223 | .env 224 | .venv 225 | env/ 226 | venv/ 227 | ENV/ 228 | env.bak/ 229 | venv.bak/ 230 | 231 | # Spyder project settings 232 | .spyderproject 233 | .spyproject 234 | 235 | # Rope project settings 236 | .ropeproject 237 | 238 | # mkdocs documentation 239 | /site 240 | 241 | # mypy 242 | .mypy_cache/ 243 | .dmypy.json 244 | dmypy.json 245 | 246 | ### Python Patch ### 247 | .venv/ 248 | 249 | ### Python.VirtualEnv Stack ### 250 | # Virtualenv 251 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 252 | [Bb]in 253 | [Ii]nclude 254 | [Ll]ib 255 | [Ll]ib64 256 | [Ll]ocal 257 | [Ss]cripts 258 | pyvenv.cfg 259 | pip-selfcheck.json 260 | 261 | ### SublimeText ### 262 | # Cache files for Sublime Text 263 | *.tmlanguage.cache 264 | *.tmPreferences.cache 265 | *.stTheme.cache 266 | 267 | # Workspace files are user-specific 268 | *.sublime-workspace 269 | 270 | # Project files should be checked into the repository, unless a significant 271 | # proportion of contributors will probably not be using Sublime Text 272 | # *.sublime-project 273 | 274 | # SFTP configuration file 275 | sftp-config.json 276 | 277 | # Package control specific files 278 | Package Control.last-run 279 | Package Control.ca-list 280 | Package Control.ca-bundle 281 | Package Control.system-ca-bundle 282 | Package Control.cache/ 283 | Package Control.ca-certs/ 284 | Package Control.merged-ca-bundle 285 | Package Control.user-ca-bundle 286 | oscrypto-ca-bundle.crt 287 | bh_unicode_properties.cache 288 | 289 | # Sublime-github package stores a github token in this file 290 | # https://packagecontrol.io/packages/sublime-github 291 | GitHub.sublime-settings 292 | 293 | ### Vim ### 294 | # Swap 295 | [._]*.s[a-v][a-z] 296 | [._]*.sw[a-p] 297 | [._]s[a-rt-v][a-z] 298 | [._]ss[a-gi-z] 299 | [._]sw[a-p] 300 | 301 | # Session 302 | Session.vim 303 | 304 | # Temporary 305 | .netrwhist 306 | # Auto-generated tag files 307 | tags 308 | # Persistent undo 309 | [._]*.un~ 310 | 311 | ### VisualStudioCode ### 312 | .vscode/* 313 | 314 | 315 | ### Windows ### 316 | # Windows thumbnail cache files 317 | Thumbs.db 318 | ehthumbs.db 319 | ehthumbs_vista.db 320 | 321 | # Dump file 322 | *.stackdump 323 | 324 | # Folder config file 325 | [Dd]esktop.ini 326 | 327 | # Recycle Bin used on file shares 328 | $RECYCLE.BIN/ 329 | 330 | # Windows Installer files 331 | *.cab 332 | *.msi 333 | *.msix 334 | *.msm 335 | *.msp 336 | 337 | # Windows shortcuts 338 | *.lnk 339 | 340 | ### VisualStudio ### 341 | ## Ignore Visual Studio temporary files, build results, and 342 | ## files generated by popular Visual Studio add-ons. 343 | ## 344 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 345 | 346 | # User-specific files 347 | *.suo 348 | *.user 349 | *.userosscache 350 | *.sln.docstates 351 | 352 | # User-specific files (MonoDevelop/Xamarin Studio) 353 | *.userprefs 354 | 355 | # Build results 356 | [Dd]ebug/ 357 | [Dd]ebugPublic/ 358 | [Rr]elease/ 359 | [Rr]eleases/ 360 | x64/ 361 | x86/ 362 | bld/ 363 | [Bb]in/ 364 | [Oo]bj/ 365 | [Ll]og/ 366 | 367 | # Visual Studio 2015/2017 cache/options directory 368 | .vs/ 369 | # Uncomment if you have tasks that create the project's static files in wwwroot 370 | #wwwroot/ 371 | 372 | # Visual Studio 2017 auto generated files 373 | Generated\ Files/ 374 | 375 | # MSTest test Results 376 | [Tt]est[Rr]esult*/ 377 | [Bb]uild[Ll]og.* 378 | 379 | # NUNIT 380 | *.VisualState.xml 381 | TestResult.xml 382 | 383 | # Build Results of an ATL Project 384 | [Dd]ebugPS/ 385 | [Rr]eleasePS/ 386 | dlldata.c 387 | 388 | # Benchmark Results 389 | BenchmarkDotNet.Artifacts/ 390 | 391 | # .NET Core 392 | project.lock.json 393 | project.fragment.lock.json 394 | artifacts/ 395 | 396 | # StyleCop 397 | StyleCopReport.xml 398 | 399 | # Files built by Visual Studio 400 | *_i.c 401 | *_p.c 402 | *_h.h 403 | *.ilk 404 | *.meta 405 | *.obj 406 | *.iobj 407 | *.pch 408 | *.pdb 409 | *.ipdb 410 | *.pgc 411 | *.pgd 412 | *.rsp 413 | *.sbr 414 | *.tlb 415 | *.tli 416 | *.tlh 417 | *.tmp 418 | *.tmp_proj 419 | *.vspscc 420 | *.vssscc 421 | .builds 422 | *.pidb 423 | *.svclog 424 | *.scc 425 | 426 | # Chutzpah Test files 427 | _Chutzpah* 428 | 429 | # Visual C++ cache files 430 | ipch/ 431 | *.aps 432 | *.ncb 433 | *.opendb 434 | *.opensdf 435 | *.sdf 436 | *.cachefile 437 | *.VC.db 438 | *.VC.VC.opendb 439 | 440 | # Visual Studio profiler 441 | *.psess 442 | *.vsp 443 | *.vspx 444 | *.sap 445 | 446 | # Visual Studio Trace Files 447 | *.e2e 448 | 449 | # TFS 2012 Local Workspace 450 | $tf/ 451 | 452 | # Guidance Automation Toolkit 453 | *.gpState 454 | 455 | # ReSharper is a .NET coding add-in 456 | _ReSharper*/ 457 | *.[Rr]e[Ss]harper 458 | *.DotSettings.user 459 | 460 | # JustCode is a .NET coding add-in 461 | .JustCode 462 | 463 | # TeamCity is a build add-in 464 | _TeamCity* 465 | 466 | # DotCover is a Code Coverage Tool 467 | *.dotCover 468 | 469 | # AxoCover is a Code Coverage Tool 470 | .axoCover/* 471 | !.axoCover/settings.json 472 | 473 | # Visual Studio code coverage results 474 | *.coverage 475 | *.coveragexml 476 | 477 | # NCrunch 478 | _NCrunch_* 479 | .*crunch*.local.xml 480 | nCrunchTemp_* 481 | 482 | # MightyMoose 483 | *.mm.* 484 | AutoTest.Net/ 485 | 486 | # Web workbench (sass) 487 | .sass-cache/ 488 | 489 | # Installshield output folder 490 | [Ee]xpress/ 491 | 492 | # DocProject is a documentation generator add-in 493 | DocProject/buildhelp/ 494 | DocProject/Help/*.HxT 495 | DocProject/Help/*.HxC 496 | DocProject/Help/*.hhc 497 | DocProject/Help/*.hhk 498 | DocProject/Help/*.hhp 499 | DocProject/Help/Html2 500 | DocProject/Help/html 501 | 502 | # Click-Once directory 503 | publish/ 504 | 505 | # Publish Web Output 506 | *.[Pp]ublish.xml 507 | *.azurePubxml 508 | # Note: Comment the next line if you want to checkin your web deploy settings, 509 | # but database connection strings (with potential passwords) will be unencrypted 510 | *.pubxml 511 | *.publishproj 512 | 513 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 514 | # checkin your Azure Web App publish settings, but sensitive information contained 515 | # in these scripts will be unencrypted 516 | PublishScripts/ 517 | 518 | # NuGet Packages 519 | *.nupkg 520 | # The packages folder can be ignored because of Package Restore 521 | **/[Pp]ackages/* 522 | # except build/, which is used as an MSBuild target. 523 | !**/[Pp]ackages/build/ 524 | # Uncomment if necessary however generally it will be regenerated when needed 525 | #!**/[Pp]ackages/repositories.config 526 | # NuGet v3's project.json files produces more ignorable files 527 | *.nuget.props 528 | *.nuget.targets 529 | 530 | # Microsoft Azure Build Output 531 | csx/ 532 | *.build.csdef 533 | 534 | # Microsoft Azure Emulator 535 | ecf/ 536 | rcf/ 537 | 538 | # Windows Store app package directories and files 539 | AppPackages/ 540 | BundleArtifacts/ 541 | Package.StoreAssociation.xml 542 | _pkginfo.txt 543 | *.appx 544 | 545 | # Visual Studio cache files 546 | # files ending in .cache can be ignored 547 | *.[Cc]ache 548 | # but keep track of directories ending in .cache 549 | !*.[Cc]ache/ 550 | 551 | # Others 552 | ClientBin/ 553 | ~$* 554 | *.dbmdl 555 | *.dbproj.schemaview 556 | *.jfm 557 | *.pfx 558 | *.publishsettings 559 | orleans.codegen.cs 560 | 561 | # Including strong name files can present a security risk 562 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 563 | #*.snk 564 | 565 | # Since there are multiple workflows, uncomment next line to ignore bower_components 566 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 567 | #bower_components/ 568 | 569 | # RIA/Silverlight projects 570 | Generated_Code/ 571 | 572 | # Backup & report files from converting an old project file 573 | # to a newer Visual Studio version. Backup files are not needed, 574 | # because we have git ;-) 575 | _UpgradeReport_Files/ 576 | Backup*/ 577 | UpgradeLog*.XML 578 | UpgradeLog*.htm 579 | ServiceFabricBackup/ 580 | *.rptproj.bak 581 | 582 | # SQL Server files 583 | *.mdf 584 | *.ldf 585 | *.ndf 586 | 587 | # Business Intelligence projects 588 | *.rdl.data 589 | *.bim.layout 590 | *.bim_*.settings 591 | *.rptproj.rsuser 592 | 593 | # Microsoft Fakes 594 | FakesAssemblies/ 595 | 596 | # GhostDoc plugin setting file 597 | *.GhostDoc.xml 598 | 599 | # Node.js Tools for Visual Studio 600 | .ntvs_analysis.dat 601 | node_modules/ 602 | 603 | # Visual Studio 6 build log 604 | *.plg 605 | 606 | # Visual Studio 6 workspace options file 607 | *.opt 608 | 609 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 610 | *.vbw 611 | 612 | # Visual Studio LightSwitch build output 613 | **/*.HTMLClient/GeneratedArtifacts 614 | **/*.DesktopClient/GeneratedArtifacts 615 | **/*.DesktopClient/ModelManifest.xml 616 | **/*.Server/GeneratedArtifacts 617 | **/*.Server/ModelManifest.xml 618 | _Pvt_Extensions 619 | 620 | # Paket dependency manager 621 | .paket/paket.exe 622 | paket-files/ 623 | 624 | # FAKE - F# Make 625 | .fake/ 626 | 627 | # JetBrains Rider 628 | .idea/ 629 | *.sln.iml 630 | 631 | # CodeRush 632 | .cr/ 633 | 634 | # Python Tools for Visual Studio (PTVS) 635 | *.pyc 636 | 637 | # Cake - Uncomment if you are using it 638 | # tools/** 639 | # !tools/packages.config 640 | 641 | # Tabs Studio 642 | *.tss 643 | 644 | # Telerik's JustMock configuration file 645 | *.jmconfig 646 | 647 | # BizTalk build output 648 | *.btp.cs 649 | *.btm.cs 650 | *.odx.cs 651 | *.xsd.cs 652 | 653 | # OpenCover UI analysis results 654 | OpenCover/ 655 | 656 | # Azure Stream Analytics local run output 657 | ASALocalRun/ 658 | 659 | # MSBuild Binary and Structured Log 660 | *.binlog 661 | 662 | # NVidia Nsight GPU debugger configuration file 663 | *.nvuser 664 | 665 | # MFractors (Xamarin productivity tool) working folder 666 | .mfractor/ 667 | 668 | # Local History for Visual Studio 669 | .localhistory/ 670 | 671 | 672 | # End of https://www.gitignore.io/api/vim,osx,linux,macos,python,windows,pycharm,sublimetext,visualstudio,visualstudiocode 673 | # Personal development files 674 | .pyre/* 675 | docs/source/_build 676 | import.py 677 | me.jpeg 678 | mac.jpeg 679 | pip-wheel-metadata/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # - "3.6" 4 | - "3.7-dev" 5 | 6 | # command to install dependencies 7 | install: 8 | - pip install -r requirements.txt 9 | - pip install codecov pytest-cov 10 | 11 | # command to run tests 12 | script: 13 | - pytest 14 | - pytest --cov=./ 15 | 16 | after_success: 17 | - codecov 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased] 4 | 5 | - Added global template support for `Coo.schedule(template=template)`. 6 | - Added an additional check for the type of the Twitter credentials. 7 | - DEV: removed Type Check from the project. 8 | 9 | ## [0.1.3] - 2018-12-13 10 | 11 | - Added support to post updates randomly on `Coo.tweet(aleatory=True)`. 12 | - Added support for updates with a single media file for all tweets on `Coo.tweet()`. 13 | - Added support for updates with a single media file for all tweets on `Coo.schedule()`. 14 | - Added support for updates with a different media file for each tweet on `Coo.schedule()`. 15 | 16 | ## [0.1.2] - 2018-11-29 17 | 18 | - Fixed template overwriting the updates when `$message` is not provided. 19 | - Fixed lots of typos. 20 | 21 | ## [0.1.1] - 2018-11-21 22 | 23 | - Fixed typos and README_P.rst for PyPI 24 | 25 | ## [0.1.0] - 2018-11-21 26 | 27 | Initial Release 28 | 29 | [Unreleased]: https://github.com/wilfredinni/coo/tree/master 30 | [0.1.3]: https://github.com/wilfredinni/coo/releases/tag/0.1.3 31 | [0.1.2]: https://github.com/wilfredinni/coo/releases/tag/0.1.2 32 | [0.1.1]: https://github.com/wilfredinni/coo/releases/tag/0.1.1 33 | [0.1.0]: https://github.com/wilfredinni/coo/releases/tag/0.1.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Carlos Montecinos Geisse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 3 |

4 | 5 |

6 | coo: schedule Twitter updates with easy 7 |

8 | 9 |

10 | 11 | PyPI version 12 | 13 | 14 | Build Status 15 | 16 | 17 | codecov 18 | 19 | 20 | Docs 21 | 22 | 23 | License 24 | 25 |

26 | 27 | Coo is an easy to use Python library for scheduling Twitter updates. To use it, you need to first apply for a developer account in the [Twitter Developers Platform](https://developer.twitter.com/) and generate the Keys and Access Tokens. 28 | 29 | 30 | ```shell 31 | pip install coo 32 | ``` 33 | 34 | Initializing 35 | 36 | ```python 37 | from coo import Coo 38 | 39 | at = Coo( 40 | "consumer_key", 41 | "consumer_secret", 42 | "access_token", 43 | "access_token_secret", 44 | preview=False 45 | ) 46 | ``` 47 | 48 | Alternatively, you can set `preview=True` and print your tweets in the terminal instead to post them on Twitter. 49 | 50 | Scheduling Twitter updates: 51 | 52 | ```python 53 | from coo import Coo 54 | 55 | at = Coo( 56 | "consumer_key", 57 | "consumer_secret", 58 | "access_token", 59 | "access_token_secret" 60 | ) 61 | 62 | tweets = [ 63 | ("2030-10-28 18:50", template, "My Twitter update with a template."), 64 | ("2030-10-29 18:15", template2, "Update with a different template."), 65 | ("2030-11-01 13:45", None, "Awesome Twitter update without a template."), 66 | ] 67 | 68 | at.schedule(tweets, time_zone="America/Santiago") 69 | ``` 70 | 71 | Or you can use a list of strings and add a `delay`, `interval` and a `template`: 72 | 73 | ```python 74 | tweets = [ 75 | "My first awesome Twitter Update", 76 | "My second awesome Twitter Update", 77 | "My third awesome Twitter Update", 78 | "My fourth awesome Twitter Update", 79 | "My fifth awesome Twitter Update", 80 | "My sixth awesome Twitter Update", 81 | ] 82 | 83 | at.tweet(tweets, delay="13:45", interval="four_hours", template=my_template) 84 | ``` 85 | 86 | For more detailed options and usage, keep reading or check the [documentation](https://coo.readthedocs.io/en/latest/) :blue_book:. 87 | 88 | 89 | ## Scheduling Twitter Updates 90 | 91 | Schedule updates with `datetime` strings or integers and use [custom templates](#Templates) if needed. 92 | 93 | ```python 94 | Coo.schedule(updates, time_zone, media) 95 | ``` 96 | 97 | Full example: 98 | 99 | ```python 100 | from coo import Coo 101 | 102 | at = Coo( 103 | "consumer_key", 104 | "consumer_secret", 105 | "access_token", 106 | "access_token_secret" 107 | ) 108 | 109 | tweets = [ 110 | # datetime with and without templates 111 | ("2030-10-28 18:50", template, "My Twitter update with a template."), 112 | ("2030-10-29 18:15", template2, "Update with a different template."), 113 | ("2030-11-01 13:45", None, "Awesome Twitter update without a template."), 114 | 115 | # date with and without templates 116 | ("2030-12-25", template3, "Merry christmas!"), 117 | ("2031-01-01", None, "And a happy new year!"), 118 | 119 | # time with and without templates 120 | ("18:46", template2, "Will be post today at 18:46."), 121 | ("23:00", None, "A tweet for today at 23:00."), 122 | 123 | # integer (seconds) with and without templates 124 | (3600, template, "This tweet will be posted in an hour."), 125 | (86400, None, "This one, tomorrow at the same hour."), 126 | ] 127 | 128 | at.schedule(tweets, time_zone="America/Santiago") 129 | ``` 130 | 131 | #### Notes for parsing DateTime strings 132 | 133 | - If a time zone is not specified, it will set to `local`. 134 | - The time will be set to 00:00:00 if it's not specified. 135 | - When passing only time information the date will default to today. 136 | - A future date is needed, otherwise a `ScheduleError` is raised. 137 | 138 | Here you can find all the [Time Zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) available. 139 | 140 | #### Media files 141 | 142 | There are two ways to add media files to your tweets. The first and easiest is to use one global file for all the updates: 143 | 144 | ```python 145 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/file.png") 146 | ``` 147 | 148 | Also, an individual file can be set for each one of the updates: 149 | 150 | ```python 151 | tweets = [ 152 | ("2030-10-28 18:50", template, "Update with an image.", "pics/owl.png"), 153 | ("2030-10-29 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 154 | ("2030-11-01 13:45", template, "Tweet without media."), 155 | ] 156 | ``` 157 | 158 | Finally, it is possible to combine these to ways. For example, if most of the tweets are gonna use the same media and just a few will have a different or none: 159 | 160 | ```python 161 | tweets = [ 162 | ("2030-11-01 13:45", template, "Tweet with global media."), 163 | ("2030-11-02 13:45", template, "Tweet with global media."), 164 | ("2030-11-03 13:45", template, "Tweet with global media."), 165 | ("2030-11-04 13:45", template, "Tweet with global media."), 166 | ("2030-11-05 13:45", template, "Tweet with global media."), 167 | ("2030-11-06 13:45", template, "Tweet with global media."), 168 | ("2030-11-07 13:45", template, "Tweet with global media."), 169 | ("2030-11-08 13:45", template, "Tweet without media.", None), 170 | ("2030-11-09 13:45", template, "Tweet without media.", None), 171 | ("2030-12-10 18:50", template, "Update with an image.", "pics/owl.png"), 172 | ("2030-12-11 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 173 | ] 174 | 175 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/global_media.png") 176 | ``` 177 | 178 | ## Tweet a list of strings 179 | 180 | Post ordered updates with `delay`, `interval`, and a [template](#Templates) if needed. 181 | 182 | ```python 183 | Coo.tweet(updates, delay, interval, template, media, time_zone, aleatory) 184 | ``` 185 | 186 | ```python 187 | from coo import Coo 188 | 189 | at = Coo( 190 | "consumer_key", 191 | "consumer_secret", 192 | "access_token", 193 | "access_token_secret" 194 | ) 195 | 196 | tweets = [ 197 | "My first awesome Twitter Update", 198 | "My second awesome Twitter Update", 199 | "My third awesome Twitter Update", 200 | "My fourth awesome Twitter Update", 201 | "My fifth awesome Twitter Update", 202 | "My sixth awesome Twitter Update", 203 | ] 204 | 205 | # post the twitter updates 206 | at.tweet(tweets) 207 | ``` 208 | 209 | #### Delay 210 | 211 | You can use `datetime`, `date` and `time` strings, integers as seconds and some strings as [keywords](#Delay-and-Interval-Keywords): `half_hour`, `one_hour`, `one_day` and `one_week` between others to delay the post of your first update. 212 | 213 | ```python 214 | # datetime, date and time strings 215 | at.tweet(tweets, delay="2030-11-24 13:45", time_zone="America/Santiago") 216 | at.tweet(tweets, delay="2030-11-24", time_zone="Australia/Sydney") 217 | at.tweet(tweets, delay="13:45", time_zone="America/New_York") 218 | 219 | # "keywords" 220 | at.tweet(tweets, delay="one_week") 221 | 222 | # integer 223 | at.tweet(tweets, delay=604800) 224 | ``` 225 | 226 | Remember to read the [Notes for parsing DateTime strings](#Notes-for-parsing-DateTime-strings). 227 | 228 | #### Interval 229 | 230 | Use integers as seconds or some strings as [keywords](#Delay-and-Interval-Keywords): `half_hour`, `one_hour`, `one_day` and `one_week` between others. 231 | 232 | ```python 233 | # "keywords" 234 | at.tweet(tweets, interval="four_hours") 235 | 236 | # integers 237 | at.tweet(tweets, interval=14400) 238 | ``` 239 | 240 | #### Template 241 | 242 | And of course, you can also set one [template](#Templates) for each one of the updates. 243 | 244 | ```python 245 | at.tweet(tweets, template=template) 246 | ``` 247 | 248 | #### Media files 249 | 250 | Use one media file for all of your updates: 251 | 252 | ```python 253 | at.tweet(tweets, media="path/to/media.jpeg") 254 | ``` 255 | 256 | #### Random updates 257 | 258 | To tweet your updates randomly: 259 | 260 | ```python 261 | at.tweet(tweets, aleatory=True) 262 | ``` 263 | 264 | #### Delay and Interval Keywords 265 | 266 | | Keyword | Seconds | 267 | | ---------------- | ------- | 268 | | now | 0 | 269 | | half_hour | 1800 | 270 | | one_hour | 3600 | 271 | | two_hours | 7200 | 272 | | four_hours | 14400 | 273 | | six_hours | 21600 | 274 | | eight_hours | 28800 | 275 | | ten_hours | 36000 | 276 | | twelve_hours | 43200 | 277 | | fourteen_hours | 50400 | 278 | | sixteen_hours | 57600 | 279 | | eighteen_hours | 64800 | 280 | | twenty_hours | 72000 | 281 | | twenty_two_hours | 79200 | 282 | | one_day | 86400 | 283 | | two_days | 172800 | 284 | | three_days | 259200 | 285 | | four_days | 345600 | 286 | | five_days | 432000 | 287 | | six_days | 518400 | 288 | | one_week | 604800 | 289 | 290 | 291 | ## Templates 292 | 293 | Templates are very simple, just use a multiline string and add a `$message` where you want your message to appear. 294 | 295 | ```python 296 | template = """My awesome header 297 | 298 | $message 299 | 300 | #python #coding #coo 301 | """ 302 | ``` 303 | 304 | ## The Twitter API 305 | 306 | Coo is written using the [Python Twitter](https://github.com/bear/python-twitter) wrapper, and through `Coo.api` you gain access to all of his models: 307 | 308 | ```python 309 | # get your followers 310 | followers = at.api.GetFollowers() 311 | 312 | # get your direct messages 313 | d_messages = at.api.GetDirectMessages() 314 | 315 | # favorited tweets 316 | favorites = at.api.GetFavorites() 317 | 318 | # mentions 319 | mentions = at.api.GetMentions() 320 | 321 | # retweets 322 | retweets = at.api.GetRetweets() 323 | ``` 324 | 325 | And a lot more. If you are interested, check their [documentation](https://python-twitter.readthedocs.io/en/latest/index.html). 326 | 327 | ## TODO's 328 | 329 | - [x] Add support for random updates. 330 | - [x] Add support for media files. 331 | - [x] Add support for multiple media files. 332 | - [ ] Add support for a history of tweets. 333 | - [ ] Add support for media files from URLs. 334 | - [ ] Add support for one template for all updates on `Coo.schedule`. 335 | - [ ] Support `.toml` files for configuration and tweets. 336 | - [ ] Support resume after a process restart (see [apscheduler](https://github.com/agronholm/apscheduler)). 337 | - [ ] Add a CLI. 338 | 339 | ## Documentation 340 | 341 | Documentation available at [coo.readthedocs.io](https://coo.readthedocs.io/en/latest/). 342 | -------------------------------------------------------------------------------- /README_P.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | coo: Schedule Twitter updates 3 | ============================= 4 | 5 | .. image:: https://badge.fury.io/py/coo.svg 6 | :target: https://badge.fury.io/py/coo 7 | .. image:: https://travis-ci.org/wilfredinni/coo.svg?branch=master 8 | :target: https://travis-ci.org/wilfredinni/coo 9 | .. image:: https://codecov.io/gh/wilfredinni/coo/branch/master/graph/badge.svg 10 | :target: https://codecov.io/gh/wilfredinni/coo 11 | .. image:: https://readthedocs.org/projects/coo/badge/?version=latest 12 | :target: https://coo.readthedocs.io/en/latest/?badge=latest 13 | .. image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg 14 | :target: https://opensource.org/licenses/Apache-2.0 15 | 16 | Coo is an easy to use Python library for scheduling Twitter updates. To use it, you need 17 | to first apply for a developer account in the 18 | `Twitter Developers Platform `_ and generate the Keys and 19 | Access Tokens. 20 | 21 | .. code-block:: python 22 | 23 | pip install coo 24 | 25 | Initializing 26 | 27 | .. code-block:: python 28 | 29 | from coo import Coo 30 | 31 | at = Coo( 32 | "consumer_key", 33 | "consumer_secret", 34 | "access_token", 35 | "access_token_secret", 36 | preview=False, 37 | ) 38 | 39 | Alternatively, you can set ``preview=True`` and print your tweets in the terminal instead 40 | to post them on Twitter. 41 | 42 | Scheduling Twitter updates: 43 | 44 | 45 | .. code-block:: python 46 | 47 | from coo import Coo 48 | 49 | at = Coo( 50 | "consumer_key", 51 | "consumer_secret", 52 | "access_token", 53 | "access_token_secret" 54 | ) 55 | 56 | tweets = [ 57 | ("2030-12-05 16:30", template, "Awesome Twitter update."), 58 | ("2030-10-28 18:50", template, "Another awesome Twitter update."), 59 | ("2030-10-29 18:15", template2, "One more update."), 60 | ("2030-11-01 13:45", None, "Twitter update without a template."), 61 | 62 | at.schedule(tweets, time_zone="America/Santiago") 63 | 64 | Or you can use a list of strings and add a ``delay``, ``interval`` and a ``template``: 65 | 66 | .. code-block:: python 67 | 68 | tweets = [ 69 | "My first awesome Twitter Update", 70 | "My second awesome Twitter Update", 71 | "My third awesome Twitter Update", 72 | "My fourth awesome Twitter Update", 73 | "My fifth awesome Twitter Update", 74 | "My sixth awesome Twitter Update", 75 | ] 76 | 77 | at.tweet(tweets, delay="13:45", interval="four_hours", template=my_template) 78 | 79 | Schedule Twitter Updates 80 | ======================== 81 | 82 | Schedule updates with `datetime` strings or integers and use custom a `Template`_ if needed. 83 | 84 | .. code-block:: python 85 | 86 | Coo.schedule(updates, time_zone) 87 | 88 | Full example: 89 | 90 | .. code-block:: python 91 | 92 | from coo import Coo 93 | 94 | at = Coo( 95 | "consumer_key", 96 | "consumer_secret", 97 | "access_token", 98 | "access_token_secret" 99 | ) 100 | 101 | tweets = [ 102 | # datetime with and without templates 103 | ("2030-10-28 18:50", template, "My Twitter update with a template."), 104 | ("2030-10-29 18:15", template2, "Update with a different template."), 105 | ("2030-11-01 13:45", None, "Twitter update without a template."), 106 | 107 | # date with and without templates 108 | ("2030-12-25", template3, "Merry christmas!"), 109 | ("2031-01-01", None, "And a happy new year!"), 110 | 111 | # time with and without templates 112 | ("18:46", template2, "Will be post today at 18:46."), 113 | ("23:00", None, "A tweet for today at 23:00."), 114 | 115 | # integer (seconds) with and without templates 116 | (3600, template, "This tweet will be posted in an hour."), 117 | (86400, None, "This one, tomorrow at the same hour."), 118 | ] 119 | 120 | at.schedule(tweets, time_zone="America/Santiago") 121 | 122 | Parsing DateTime strings 123 | ^^^^^^^^^^^^^^^^^^^^^^^^ 124 | 125 | - If a time zone is not specified, it will set to `local`. 126 | - The time will be set to 00:00:00 if it's not specified. 127 | - When passing only time information the date will default to today. 128 | - A future date is needed, otherwise a `ScheduleError` is raised. 129 | 130 | Here you can find all the 131 | `Time Zones `_. 132 | 133 | Media Files 134 | ^^^^^^^^^^^ 135 | 136 | There are two ways to add media files to your tweets. The first and easiest is to use one global file for all the updates: 137 | 138 | .. code-block:: python 139 | 140 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/file.png") 141 | 142 | Also, an individual file can be set for each one of the updates: 143 | 144 | .. code-block:: python 145 | 146 | tweets = [ 147 | ("2030-10-28 18:50", template, "Update with an image.", "pics/owl.png"), 148 | ("2030-10-29 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 149 | ("2030-11-01 13:45", template, "Tweet without media."), 150 | ] 151 | 152 | Finally, it is possible to combine these to ways. For example, if most of the tweets are gonna use the same media and just a few will have a different or none: 153 | 154 | .. code-block:: python 155 | 156 | tweets = [ 157 | ("2030-11-01 13:45", template, "Tweet with global media."), 158 | ("2030-11-02 13:45", template, "Tweet with global media."), 159 | ("2030-11-03 13:45", template, "Tweet with global media."), 160 | ("2030-11-04 13:45", template, "Tweet with global media."), 161 | ("2030-11-05 13:45", template, "Tweet with global media."), 162 | ("2030-11-06 13:45", template, "Tweet with global media."), 163 | ("2030-11-07 13:45", template, "Tweet with global media."), 164 | ("2030-11-08 13:45", template, "Tweet without media.", None), 165 | ("2030-11-09 13:45", template, "Tweet without media.", None), 166 | ("2030-12-10 18:50", template, "Update with an image.", "pics/owl.png"), 167 | ("2030-12-11 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 168 | ] 169 | 170 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/global_media.png") 171 | 172 | 173 | Tweet an ordered list of strings 174 | ================================ 175 | 176 | Post ordered updates with `Delay`_, `Interval`_, and a `Template`_ if needed. 177 | 178 | .. code-block:: python 179 | 180 | Coo.tweet(updates, delay, interval, template, time_zone) 181 | 182 | .. code-block:: python 183 | 184 | from coo import Coo 185 | 186 | at = Coo( 187 | "consumer_key", 188 | "consumer_secret", 189 | "access_token", 190 | "access_token_secret" 191 | ) 192 | 193 | tweets = [ 194 | "My first awesome Twitter Update", 195 | "My second awesome Twitter Update", 196 | "My third awesome Twitter Update", 197 | "My fourth awesome Twitter Update", 198 | "My fifth awesome Twitter Update", 199 | "My sixth awesome Twitter Update", 200 | ] 201 | 202 | # post the twitter updates 203 | at.tweet(tweets) 204 | 205 | Delay 206 | ^^^^^ 207 | 208 | You can use ``datetime``, ``date`` and ``time`` strings, integers as seconds and some 209 | `Keywords`_: ``half_hour``, ``one_hour``, ``one_day`` and ``one_week`` between others to 210 | delay the post of your first update. 211 | 212 | .. code-block:: python 213 | 214 | # datetime, date and time strings 215 | at.tweet(tweets, delay="2030-11-24 13:45", time_zone="America/Santiago") 216 | at.tweet(tweets, delay="2030-11-24", time_zone="Australia/Sydney") 217 | at.tweet(tweets, delay="13:45", time_zone="America/New_York") 218 | 219 | # "keywords" 220 | at.tweet(tweets, delay="one_week") 221 | 222 | # integer 223 | at.tweet(tweets, delay=604800) 224 | 225 | When parsing DateTime strings: 226 | 227 | - If a time zone is not specified, it will set to `local`. 228 | - The time will be set to 00:00:00 if it's not specified. 229 | - When passing only time information the date will default to today. 230 | - A future date is needed, otherwise a `ScheduleError` is raised. 231 | 232 | Here you can find all the `Time Zones `_. 233 | 234 | Interval 235 | ^^^^^^^^ 236 | 237 | Use integers as seconds or some strings as `Keywords`_: ``half_hour``, ``one_hour``, 238 | ``one_day`` and ``one_week`` between others. 239 | 240 | .. code-block:: python 241 | 242 | # "keywords" 243 | at.tweet(tweets, interval="four_hours") 244 | 245 | # integers 246 | at.tweet(tweets, interval=14400) 247 | 248 | Media files 249 | ^^^^^^^^^^^ 250 | 251 | Use one media file for all of your updates: 252 | 253 | .. code-block:: python 254 | 255 | at.tweet(tweets, media="path/to/media.jpeg") 256 | 257 | Random updates 258 | ^^^^^^^^^^^^^^ 259 | 260 | To tweet your updates randomly: 261 | 262 | .. code-block:: python 263 | 264 | at.tweet(tweets, aleatory=True) 265 | 266 | Keywords 267 | ^^^^^^^^ 268 | 269 | ================ ======= 270 | Keyword Seconds 271 | ================ ======= 272 | now 0 273 | half_hour 1800 274 | one_hour 3600 275 | two_hours 7200 276 | four_hours 14400 277 | six_hours 21600 278 | eight_hours 28800 279 | ten_hours 36000 280 | twelve_hours 43200 281 | fourteen_hours 50400 282 | sixteen_hours 57600 283 | eighteen_hours 64800 284 | twenty_hours 72000 285 | twenty_two_hours 79200 286 | one_day 86400 287 | two_days 172800 288 | three_days 259200 289 | four_days 345600 290 | five_days 432000 291 | six_days 518400 292 | one_week 604800 293 | ================ ======= 294 | 295 | Template 296 | ======== 297 | 298 | Templates are very simple, just use a multiline string and add a ``$message`` 299 | where you want your message to appear. 300 | 301 | .. code-block:: python 302 | 303 | template = """My awesome header 304 | 305 | $message 306 | 307 | #python #coding #coo 308 | """ 309 | 310 | The Twitter API 311 | =============== 312 | 313 | Coo is written using the `Python Twitter `_ 314 | wrapper, and through `Coo.api` you gain access to all of his models: 315 | 316 | .. code-block:: python 317 | 318 | # get your followers 319 | followers = at.api.GetFollowers() 320 | 321 | # get your direct messages 322 | d_messages = at.api.GetDirectMessages() 323 | 324 | # favorited tweets 325 | favorites = at.api.GetFavorites() 326 | 327 | # mentions 328 | mentions = at.api.GetMentions() 329 | 330 | # retweets 331 | retweets = at.api.GetRetweets() 332 | 333 | And a lot more. If you are interested, check their `documentation `_. 334 | 335 | Documentation 336 | ============= 337 | 338 | Documentation available at `readthedocs.org `_. -------------------------------------------------------------------------------- /coo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilfredinni/coo/e6dbfc11aadfdde27c6edbd0343adb54b849a5fc/coo.png -------------------------------------------------------------------------------- /coo/__init__.py: -------------------------------------------------------------------------------- 1 | from .coo import Coo 2 | 3 | 4 | __title__ = "coo" 5 | __license__ = "Apache 2.0" 6 | __version__ = "0.1.3" 7 | __author__ = "Carlos Montecinos Geisse" 8 | __contact__ = "carlos.w.montecinos@gmail.com" 9 | __url__ = "https://github.com/wilfredinni/coo" 10 | -------------------------------------------------------------------------------- /coo/_exceptions.py: -------------------------------------------------------------------------------- 1 | class TweetTypeError(TypeError): 2 | """ 3 | Raised when the argument provided to 'msg' is not a list of strings. 4 | """ 5 | 6 | wrongListMsg = "A List of Strings is required." 7 | 8 | 9 | class TemplateError(TypeError): 10 | """ Raised when the wrong data type is provided in a template. """ 11 | 12 | templateInfoMsg = "template must be a string." 13 | templateMsgErr = "The template must contain a '$message'." 14 | 15 | 16 | class ScheduleError(TypeError): 17 | 18 | wrongListMsg = "A list of tuples is required." 19 | tupleLenError = "Every tuple need a length of 3 or 4." 20 | pastDateError = "A future date is needed." 21 | -------------------------------------------------------------------------------- /coo/_utils.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | import time 3 | 4 | import pendulum 5 | from pendulum.parsing.exceptions import ParserError 6 | 7 | from ._exceptions import TemplateError, ScheduleError 8 | 9 | 10 | TIME_DICT = { 11 | "now": 0, 12 | "half_hour": 1800, 13 | "one_hour": 3600, 14 | "two_hours": 7200, 15 | "four_hours": 14400, 16 | "six_hours": 21600, 17 | "eight_hours": 28800, 18 | "ten_hours": 36000, 19 | "twelve_hours": 43200, 20 | "fourteen_hours": 50400, 21 | "sixteen_hours": 57600, 22 | "eighteen_hours": 64800, 23 | "twenty_hours": 72000, 24 | "twenty_two_hours": 79200, 25 | "one_day": 86400, 26 | "two_days": 172800, 27 | "three_days": 259200, 28 | "four_days": 345600, 29 | "five_days": 432000, 30 | "six_days": 518400, 31 | "one_week": 604800, 32 | } 33 | 34 | 35 | def parse_time(date_time, time_zone): 36 | """Returns the seconds between now and the scheduled time.""" 37 | now = pendulum.now(time_zone) 38 | update = pendulum.parse(date_time, tz=time_zone) 39 | 40 | # If a time zone is not specified, it will be set to local. 41 | # When passing only time information the date will default to today. 42 | # The time will be set to 00:00:00 if it's not specified. 43 | # A future date is needed. 44 | 45 | secs = update - now 46 | if secs.seconds < 0: 47 | raise ScheduleError(ScheduleError.pastDateError) 48 | 49 | return secs.seconds 50 | 51 | 52 | def parse_or_get(schedule_time, time_zone): 53 | """Returns seconds from dictionaries, integers or a DateTime.""" 54 | if isinstance(schedule_time, int): 55 | return schedule_time 56 | elif schedule_time in TIME_DICT: 57 | return TIME_DICT.get(schedule_time) 58 | 59 | try: 60 | return parse_time(schedule_time, time_zone) 61 | except ParserError: 62 | raise TypeError("An integer, valid datetime or keyword is needed.") 63 | 64 | 65 | def zzz(sleep_time, time_zone=None): 66 | """Delay sleep and interval time sleep. """ 67 | try: 68 | time.sleep(sleep_time) 69 | except TypeError: 70 | sleep_time = parse_or_get(sleep_time, time_zone) 71 | time.sleep(sleep_time) 72 | 73 | 74 | def tweet_template(update, template): 75 | """Returns the the update in the template.""" 76 | # Raise an Error if the template does not contain a $message. 77 | if template and "$message" not in template: 78 | raise TemplateError(TemplateError.templateMsgErr) 79 | 80 | try: 81 | return Template(template).substitute(message=update) 82 | except TypeError: 83 | raise TemplateError(TemplateError.templateInfoMsg) 84 | -------------------------------------------------------------------------------- /coo/coo.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import random 3 | import asyncio 4 | 5 | import twitter 6 | 7 | from ._utils import zzz, tweet_template, parse_or_get 8 | from ._exceptions import ScheduleError, TweetTypeError 9 | 10 | 11 | class Coo: 12 | """ 13 | Schedule Twitter Updates with Easy. 14 | 15 | Note: to use this library you need to create an account on 16 | https://developer.twitter.com/ and generate Keys and Access 17 | Tokens. 18 | 19 | Attributes 20 | ---------- 21 | consumer : str 22 | Twitter consumer key. 23 | consumer_secret : str 24 | Twitter consumer secret. 25 | token : str 26 | Twitter token. 27 | token_secret : str 28 | Twitter token secret. 29 | preview : bool, optional 30 | Print the update(s) on the console (default is 'False). 31 | 32 | Methods 33 | ------- 34 | verify() 35 | Verify if the authentication is valid. 36 | tweet(updates, _delay=None, _interval=None, template=None, time_zone="local") 37 | Post Twitter Updates from a list of strings. 38 | schedule(updates, time_zone='local') 39 | Post multiple Twitter Updates from a list of tuples. 40 | _str_update() 41 | Post a Twitter Update from a string. 42 | """ 43 | 44 | time_zone: str = "local" 45 | media = None 46 | global_media = None 47 | global_template = None 48 | 49 | def __init__(self, consumer, consumer_secret, token, token_secret, preview=False): 50 | """ 51 | Parameters 52 | ---------- 53 | consumer : str 54 | Twitter consumer key. 55 | consumer_secret : str 56 | Twitter consumer secret. 57 | token : str 58 | Twitter token. 59 | token_secret : str 60 | Twitter token secret. 61 | preview : bool, optional 62 | Print the update(s) on the console. 63 | """ 64 | # check for correct credentials types 65 | self._check_credentials_type(consumer, consumer_secret, token, token_secret) 66 | 67 | # https://github.com/bear/python-twitter 68 | self.consumer = consumer 69 | self.consumer_secret = consumer_secret 70 | self.token = token 71 | self.token_secret = token_secret 72 | 73 | # True to preview the update in the console. 74 | self.preview = preview 75 | 76 | # _interval and _delay switches. 77 | self._delay_time = True 78 | self._interval_time = False 79 | 80 | # The async loop for the custom updates. 81 | self.loop = asyncio.get_event_loop() 82 | 83 | def _check_credentials_type(self, *args): 84 | for credential in args: 85 | if not isinstance(credential, str): 86 | raise TypeError("Twitter credentials must be strings") 87 | 88 | @property 89 | def api(self): 90 | """ 91 | Through Coo.api you gain access to all of the Python Twitter 92 | wrapper models: 93 | 94 | from coo import Coo 95 | 96 | >>> at = Coo("consumer", "consumer_secret", "access_token", "token_secret") 97 | >>> at.api.GetFollowers() 98 | 99 | More info: https://python-twitter.readthedocs.io/en/latest/index.html 100 | """ 101 | return twitter.Api( 102 | self.consumer, self.consumer_secret, self.token, self.token_secret 103 | ) 104 | 105 | @property 106 | def verify(self): 107 | """Verify if the authentication is valid.""" 108 | return self.api.VerifyCredentials() 109 | 110 | @classmethod 111 | def set_time_zone(cls, time_zone): 112 | cls.time_zone = time_zone 113 | 114 | @classmethod 115 | def set_media_file(cls, media): 116 | cls.media = media 117 | 118 | @classmethod 119 | def set_global_media_file(cls, global_media): 120 | cls.global_media = global_media 121 | 122 | @classmethod 123 | def set_global_template(cls, global_template): 124 | cls.global_template = global_template 125 | 126 | def tweet( 127 | self, 128 | updates, 129 | delay=None, 130 | interval=None, 131 | template=None, 132 | media=media, 133 | time_zone=time_zone, 134 | aleatory=False, 135 | ): 136 | """ 137 | Post Twitter Updates from a list of strings. 138 | 139 | Parameters 140 | ---------- 141 | updates : list 142 | A list of strings, each one is a Twitter Update. 143 | _delay : str, int, optional 144 | The time before the first Update. 145 | _interval : str, int, optional 146 | The time between Updates. 147 | template : str, optional 148 | A string to serve as a template. Need to has a "$message". 149 | media : str, optional 150 | PATH to a local file, or a file-like object (something 151 | with a read() method). 152 | time_zone : str, optional 153 | Sets a time zone for parsing datetime strings (default is 'local'): 154 | https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 155 | aleatory : bool, optional 156 | Tweet the updates randomly (default is 'False'). 157 | 158 | Raises 159 | ------ 160 | TweetTypeError 161 | When "updates" is not a list or its elements are not strings. 162 | """ 163 | if not isinstance(updates, list) or not isinstance(updates[0], str): 164 | raise TweetTypeError(TweetTypeError.wrongListMsg) 165 | 166 | if aleatory: 167 | random.shuffle(updates) 168 | if time_zone is not self.time_zone: 169 | self.set_time_zone(time_zone) 170 | if media: 171 | self.set_media_file(Path(media)) 172 | 173 | self._delay(delay) 174 | for update in updates: 175 | self._interval(interval) 176 | self._str_update(update, template) 177 | 178 | return updates 179 | 180 | def schedule( 181 | self, updates, time_zone=time_zone, media=media, template=global_template 182 | ): 183 | """ 184 | Post multiple Twitter Updates from a list of tuples. 185 | 186 | Parameters 187 | ---------- 188 | updates : list 189 | A list of tuples that contains: 190 | 191 | [("datetime", "template", "update msg")] 192 | 193 | e.g. 194 | 195 | [("2040-10-30 00:05", template, "Update msg")] 196 | 197 | Notes for parsing date and time strings: 198 | - If a time zone is not specified, it will be set to local. 199 | - When parsing only time information the date will default to today. 200 | - The time will be set to 00:00:00 if it's not specified. 201 | - A future date is needed, otherwise, a ScheduleError is raised. 202 | 203 | The template is string with a "$message". 204 | 205 | time_zone : str, optional 206 | Sets a time zone for parsing datetime strings (default is 'local'): 207 | https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 208 | media : str, optional 209 | PATH to a local file, or a file-like object (something 210 | with a read() method). 211 | template : str, optional 212 | A global template for all the tweets with a "None" value on "msg[1]". 213 | 214 | Raises 215 | ------ 216 | ScheduleError 217 | When the length of a tuple updates is less or greater than 3. 218 | """ 219 | if not isinstance(updates[0], tuple): 220 | raise ScheduleError(ScheduleError.wrongListMsg) 221 | if template: 222 | self.set_global_template(template) 223 | if time_zone is not self.time_zone: 224 | self.set_time_zone(time_zone) 225 | if media: 226 | self.set_global_media_file(Path(media)) 227 | 228 | self.loop.run_until_complete(self._async_tasks(updates)) 229 | self.loop.close() 230 | 231 | def _str_update(self, update, template): 232 | """ 233 | Post a Twitter Update from a string. 234 | 235 | Parameters 236 | ---------- 237 | update : str 238 | A string representing a Twitter Update. 239 | template : str, optional 240 | A string to serve as a template. Need to has a "$message". 241 | 242 | Returns 243 | ------- 244 | twitter.Api.PostUpdate 245 | Post the update to Twitter. 246 | """ 247 | if template: 248 | update = tweet_template(update=update, template=template) 249 | elif self.global_template: 250 | update = tweet_template(update=update, template=self.global_template) 251 | 252 | if self.preview: 253 | print(update) 254 | return 255 | 256 | try: 257 | # Try to post with a media file. 258 | with open(self.media, "rb") as media_file: # type: ignore 259 | return self.api.PostUpdate(update, media=media_file) 260 | except TypeError: 261 | # If media is not a readable type, just post the update. 262 | return self.api.PostUpdate(update) 263 | 264 | async def _async_tasks(self, custom_msgs): 265 | """Prepare the asyncio tasks for the custom tweets.""" 266 | for msg in set(custom_msgs): 267 | if len(msg) < 3 or len(msg) > 4: 268 | raise ScheduleError(ScheduleError.tupleLenError) 269 | 270 | await asyncio.wait( 271 | [self.loop.create_task(self._custom_updates(post)) for post in custom_msgs] 272 | ) 273 | 274 | async def _custom_updates(self, msg): 275 | """ 276 | Process custom updates: templates and updates time for every 277 | Twitter update. 278 | """ 279 | seconds = parse_or_get(msg[0], self.time_zone) 280 | await asyncio.sleep(seconds) 281 | 282 | if len(msg) == 4 and msg[3] is not None: 283 | self.set_media_file(Path(msg[3])) 284 | elif len(msg) == 3 and self.global_media: 285 | self.set_media_file(Path(self.global_media)) 286 | else: 287 | self.set_media_file(None) 288 | 289 | return self._str_update(update=msg[2], template=msg[1]) 290 | 291 | def _delay(self, _delay): 292 | """_delay the Post of one or multiple tweets.""" 293 | if _delay and self._delay_time: 294 | zzz(_delay, self.time_zone) 295 | 296 | # Set to False to avoid repetition 297 | self._delay_time = False 298 | 299 | def _interval(self, _interval): 300 | """Add an _interval between Twitter Updates.""" 301 | # Avoid the first iteration 302 | if _interval and self._interval_time is True: 303 | zzz(_interval) 304 | 305 | # Allow from the second one 306 | if self._interval_time is False: 307 | self._interval_time = True 308 | 309 | def __str__(self): 310 | return f"Twitter User: {self.verify.name}." 311 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | `0.1.3 `_ - 2018-12-13 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | **Added** 8 | 9 | - Added support to post updates randomly on ``Coo.tweet(aleartory=True)``. 10 | - Added support for updates with a single media file for all tweets on ``Coo.tweet()``. 11 | - Added support for updates with a single media file for all tweets on ``Coo.schedule()``. 12 | - Added support for updates with a different media file for each tweet on ``Coo.schedule()``. 13 | 14 | `0.1.2 `_ - 2018-11-29 15 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 16 | 17 | **Added** 18 | 19 | - Added changes to CHANGELOG.md. 20 | - Added changes to the documentation. 21 | - Added releases. 22 | 23 | **Fixed** 24 | 25 | - Fixed template overwriting the tweets when `$message` is not provided. 26 | - Fixed lots of typos. 27 | 28 | `0.1.1 `_ - 2018-11-21 29 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 30 | 31 | **Fixed** 32 | 33 | - Fixed typos and README_P.rst for PyPI. 34 | 35 | `0.1.0 `_ - 2018-11-21 36 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | Initial Release 39 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | 18 | # sys.path.insert(0, os.path.abspath("../coo")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "coo" 24 | copyright = "2018, wilfredinni" 25 | author = "wilfredinni" 26 | 27 | # The short X.Y version 28 | version = "" 29 | # The full version, including alpha/beta/rc tags 30 | release = "0.1.0" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = ["sphinx.ext.autodoc"] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ["ntemplates"] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = ".rst" 52 | 53 | # The master toctree document. 54 | master_doc = "index" 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path. 66 | exclude_patterns = [] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = None 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = "alabaster" 78 | 79 | # Theme options are theme-specific and customize the look and feel of a theme 80 | # further. For a list of options available for each theme, see the 81 | # documentation. 82 | # 83 | # html_theme_options = {} 84 | 85 | # Add any paths that contain custom static files (such as style sheets) here, 86 | # relative to this directory. They are copied after the builtin static files, 87 | # so a file named "default.css" will overwrite the builtin "default.css". 88 | html_static_path = ["nstatic"] 89 | 90 | # Custom sidebar templates, must be a dictionary that maps document names 91 | # to template names. 92 | # 93 | # The default sidebars (for documents that don't match any pattern) are 94 | # defined by theme itself. Builtin themes are using these templates by 95 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 96 | # 'searchbox.html']``. 97 | # 98 | # html_sidebars = {} 99 | 100 | 101 | # -- Options for HTMLHelp output --------------------------------------------- 102 | 103 | # Output file base name for HTML help builder. 104 | htmlhelp_basename = "coodoc" 105 | 106 | 107 | # -- Options for LaTeX output ------------------------------------------------ 108 | 109 | latex_elements = { 110 | # The paper size ('letterpaper' or 'a4paper'). 111 | # 112 | # 'papersize': 'letterpaper', 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | # Additional stuff for the LaTeX preamble. 117 | # 118 | # 'preamble': '', 119 | # Latex figure (float) alignment 120 | # 121 | # 'figure_align': 'htbp', 122 | } 123 | 124 | # Grouping the document tree into LaTeX files. List of tuples 125 | # (source start file, target name, title, 126 | # author, documentclass [howto, manual, or own class]). 127 | latex_documents = [ 128 | (master_doc, "coo.tex", "coo Documentation", "wilfredinni", "manual") 129 | ] 130 | 131 | 132 | # -- Options for manual page output ------------------------------------------ 133 | 134 | # One entry per manual page. List of tuples 135 | # (source start file, name, description, authors, manual section). 136 | man_pages = [(master_doc, "coo", "coo Documentation", [author], 1)] 137 | 138 | 139 | # -- Options for Texinfo output ---------------------------------------------- 140 | 141 | # Grouping the document tree into Texinfo files. List of tuples 142 | # (source start file, target name, title, author, 143 | # dir menu entry, description, category) 144 | texinfo_documents = [ 145 | ( 146 | master_doc, 147 | "coo", 148 | "coo Documentation", 149 | author, 150 | "coo", 151 | "One line description of project.", 152 | "Miscellaneous", 153 | ) 154 | ] 155 | 156 | 157 | # -- Options for Epub output ------------------------------------------------- 158 | 159 | # Bibliographic Dublin Core info. 160 | epub_title = project 161 | 162 | # The unique identifier of the text. This can be a ISBN number 163 | # or the project homepage. 164 | # 165 | # epub_identifier = '' 166 | 167 | # A unique identification for the text. 168 | # 169 | # epub_uid = '' 170 | 171 | # A list of files that should not be packed into the epub file. 172 | epub_exclude_files = ["search.html"] 173 | 174 | 175 | # -- Extension configuration ------------------------------------------------- 176 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | coo: Schedule Twitter updates 3 | ============================= 4 | 5 | .. raw:: html 6 | 7 | 8 |

Logo

9 | 10 | 11 | .. raw:: html 12 | 13 | 14 |

15 | 16 | PyPI version 17 | 18 | 19 | Build Status 20 | 21 | 22 | codecov 23 | 24 | 25 | License 26 | 27 |

28 | 29 | 30 | Coo is an easy to use Python library for scheduling Twitter updates. To use it, you need 31 | to first apply for a developer account in the 32 | `Twitter Developers Platform `_ and generate the Keys and 33 | Access Tokens. 34 | 35 | .. code-block:: python 36 | 37 | from coo import Coo 38 | 39 | at = Coo( 40 | "consumer_key", 41 | "consumer_secret", 42 | "access_token", 43 | "access_token_secret" 44 | ) 45 | 46 | tweets = [ 47 | ("2030-12-05 16:30", template, "Awesome Twitter update."), 48 | ("2030-10-28 18:50", template, "Another awesome Twitter update."), 49 | ("2030-10-29 18:15", template2, "One more update."), 50 | ("2030-11-01 13:45", None, "Twitter update without a template."), 51 | 52 | at.schedule(tweets, time_zone="America/Santiago") 53 | 54 | Or you can use a list of strings and add a ``delay``, ``interval`` and ``template``: 55 | 56 | .. code-block:: python 57 | 58 | tweets = [ 59 | "My first awesome Twitter Update", 60 | "My second awesome Twitter Update", 61 | "My third awesome Twitter Update", 62 | "My fourth awesome Twitter Update", 63 | "My fifth awesome Twitter Update", 64 | "My sixth awesome Twitter Update", 65 | ] 66 | 67 | at.tweet(tweets, delay="13:45", interval="four_hours", template=my_template) 68 | 69 | User Guide 70 | ^^^^^^^^^^ 71 | 72 | .. toctree:: 73 | :maxdepth: 2 74 | 75 | schedule 76 | list_schedule 77 | twitter_api 78 | changelog 79 | 80 | Indices and tables 81 | ================== 82 | 83 | * :ref:`genindex` 84 | * :ref:`modindex` 85 | * :ref:`search` 86 | -------------------------------------------------------------------------------- /docs/source/list_schedule.rst: -------------------------------------------------------------------------------- 1 | Schedule a list of strings 2 | ========================== 3 | 4 | Post ordered updates with `Delay`_, `Interval`_, and a `Template`_ if needed. 5 | 6 | .. code-block:: python 7 | 8 | Coo.tweet(updates, delay, interval, template, time_zone) 9 | 10 | .. code-block:: python 11 | 12 | from coo import Coo 13 | 14 | at = Coo( 15 | "consumer_key", 16 | "consumer_secret", 17 | "access_token", 18 | "access_token_secret" 19 | ) 20 | 21 | tweets = [ 22 | "My first awesome Twitter Update", 23 | "My second awesome Twitter Update", 24 | "My third awesome Twitter Update", 25 | "My fourth awesome Twitter Update", 26 | "My fifth awesome Twitter Update", 27 | "My sixth awesome Twitter Update", 28 | ] 29 | 30 | # post the twitter updates 31 | at.tweet(tweets) 32 | 33 | Delay 34 | ^^^^^ 35 | 36 | You can use ``datetime``, ``date`` and ``time`` strings, integers as seconds and some 37 | `Keywords`_: ``half_hour``, ``one_hour``, ``one_day`` and ``one_week`` between others to 38 | delay the post of your first update. 39 | 40 | .. code-block:: python 41 | 42 | # datetime, date and time strings 43 | at.tweet(tweets, delay="2030-11-24 13:45", time_zone="America/Santiago") 44 | at.tweet(tweets, delay="2030-11-24", time_zone="Australia/Sydney") 45 | at.tweet(tweets, delay="13:45", time_zone="America/New_York") 46 | 47 | # "keywords" 48 | at.tweet(tweets, delay="one_week") 49 | 50 | # integer 51 | at.tweet(tweets, delay=604800) 52 | 53 | .. note:: 54 | 55 | When parsing DateTime strings: 56 | 57 | - If a time zone is not specified, it will set to `local`. 58 | - The time will be set to 00:00:00 if it's not specified. 59 | - When passing only time information the date will default to today. 60 | - A future date is needed, otherwise a `ScheduleError` is raised. 61 | 62 | Here you can find all the `Time Zones `_. 63 | 64 | Interval 65 | ^^^^^^^^ 66 | 67 | Use integers as seconds or some strings as `Keywords`_: ``half_hour``, ``one_hour``, 68 | ``one_day`` and ``one_week`` between others. 69 | 70 | .. code-block:: python 71 | 72 | # "keywords" 73 | at.tweet(tweets, interval="four_hours") 74 | 75 | # integers 76 | at.tweet(tweets, interval=14400) 77 | 78 | Media files 79 | ^^^^^^^^^^^ 80 | 81 | Use one media file for all of your updates: 82 | 83 | .. code-block:: python 84 | 85 | at.tweet(tweets, media="path/to/media.jpeg") 86 | 87 | Random updates 88 | ^^^^^^^^^^^^^^ 89 | 90 | To tweet your updates randomly: 91 | 92 | .. code-block:: python 93 | 94 | at.tweet(tweets, aleatory=True) 95 | 96 | Keywords 97 | ^^^^^^^^ 98 | 99 | ================ ======= 100 | Keyword Seconds 101 | ================ ======= 102 | now 0 103 | half_hour 1800 104 | one_hour 3600 105 | two_hours 7200 106 | four_hours 14400 107 | six_hours 21600 108 | eight_hours 28800 109 | ten_hours 36000 110 | twelve_hours 43200 111 | fourteen_hours 50400 112 | sixteen_hours 57600 113 | eighteen_hours 64800 114 | twenty_hours 72000 115 | twenty_two_hours 79200 116 | one_day 86400 117 | two_days 172800 118 | three_days 259200 119 | four_days 345600 120 | five_days 432000 121 | six_days 518400 122 | one_week 604800 123 | ================ ======= 124 | 125 | Template 126 | ^^^^^^^^ 127 | You can also set one template for each one of the updates. 128 | 129 | .. code-block:: python 130 | 131 | at.tweet(tweets, template=template) 132 | 133 | Templates are very simple, just use a multiline string and add a `$message` where you want your message to appear. 134 | 135 | .. code-block:: python 136 | 137 | template = """My aswesome header 138 | 139 | $message 140 | 141 | #python #coding #coo 142 | """ -------------------------------------------------------------------------------- /docs/source/schedule.rst: -------------------------------------------------------------------------------- 1 | Schedule Twitter Updates 2 | ======================== 3 | 4 | Schedule updates with `datetime` strings or integers and use custom `Templates`_ if needed. 5 | 6 | .. code-block:: python 7 | 8 | Coo.schedule(updates, time_zone) 9 | 10 | Full example: 11 | 12 | .. code-block:: python 13 | 14 | from coo import Coo 15 | 16 | at = Coo( 17 | "consumer_key", 18 | "consumer_secret", 19 | "access_token", 20 | "access_token_secret" 21 | ) 22 | 23 | tweets = [ 24 | # datetime with and without templates 25 | ("2030-10-28 18:50", template, "My Twitter update with a template."), 26 | ("2030-10-29 18:15", template2, "Update with a different template."), 27 | ("2030-11-01 13:45", None, "Twitter update without a template."), 28 | 29 | # date with and without templates 30 | ("2030-12-25", template3, "Merry christmas!"), 31 | ("2031-01-01", None, "And a happy new year!"), 32 | 33 | # time with and without templates 34 | ("18:46", template2, "Will be post today at 18:46."), 35 | ("23:00", None, "A tweet for today at 23:00."), 36 | 37 | # integer (seconds) with and without templates 38 | (3600, template, "This tweet will be posted in an hour."), 39 | (86400, None, "This one, tomorrow at the same hour."), 40 | ] 41 | 42 | at.schedule(tweets, time_zone="America/Santiago") 43 | 44 | Parsing DateTime strings 45 | ^^^^^^^^^^^^^^^^^^^^^^^^ 46 | 47 | .. note:: 48 | 49 | - If a time zone is not specified, it will set to `local`. 50 | - The time will be set to 00:00:00 if it's not specified. 51 | - When passing only time information the date will default to today. 52 | - A future date is needed, otherwise a `ScheduleError` is raised. 53 | 54 | Here you can find all the `Time Zones `_. 55 | 56 | Media Files 57 | ^^^^^^^^^^^ 58 | 59 | There are two ways to add media files to your tweets. The first and easiest is to use one global file for all the updates: 60 | 61 | .. code-block:: python 62 | 63 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/file.png") 64 | 65 | Also, an individual file can be set for each one of the updates: 66 | 67 | .. code-block:: python 68 | 69 | tweets = [ 70 | ("2030-10-28 18:50", template, "Update with an image.", "pics/owl.png"), 71 | ("2030-10-29 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 72 | ("2030-11-01 13:45", template, "Tweet without media."), 73 | ] 74 | 75 | Finally, it is possible to combine these to ways. For example, if most of the tweets are gonna use the same media and just a few will have a different or none: 76 | 77 | .. code-block:: python 78 | 79 | tweets = [ 80 | ("2030-11-01 13:45", template, "Tweet with global media."), 81 | ("2030-11-02 13:45", template, "Tweet with global media."), 82 | ("2030-11-03 13:45", template, "Tweet with global media."), 83 | ("2030-11-04 13:45", template, "Tweet with global media."), 84 | ("2030-11-05 13:45", template, "Tweet with global media."), 85 | ("2030-11-06 13:45", template, "Tweet with global media."), 86 | ("2030-11-07 13:45", template, "Tweet with global media."), 87 | ("2030-11-08 13:45", template, "Tweet without media.", None), 88 | ("2030-11-09 13:45", template, "Tweet without media.", None), 89 | ("2030-12-10 18:50", template, "Update with an image.", "pics/owl.png"), 90 | ("2030-12-11 18:15", template, "Update with other media.", "videos/funny_video.mp4"), 91 | ] 92 | 93 | at.schedule(tweets, time_zone="America/Santiago", media="path/to/global_media.png") 94 | 95 | Templates 96 | ^^^^^^^^^ 97 | 98 | You can set different templates for each one of your updates, or none. 99 | 100 | .. code-block:: python 101 | 102 | tweets = [ 103 | # datetime with and without templates 104 | ("2030-10-28 18:50", template, "My Twitter update with a template."), 105 | ("2030-10-29 18:15", template2, "Update with a different template."), 106 | ("2030-11-01 13:45", None, "Twitter update without a template."), 107 | 108 | ] 109 | 110 | at.schedule(tweets, time_zone="America/Santiago") 111 | 112 | Templates are very simple, just use a multiline string and add a `$message` where you want your message to appear. 113 | 114 | .. code-block:: python 115 | 116 | template = """My aswesome header 117 | 118 | $message 119 | 120 | #python #coding #coo 121 | """ -------------------------------------------------------------------------------- /docs/source/twitter_api.rst: -------------------------------------------------------------------------------- 1 | The Twitter API 2 | =============== 3 | 4 | Coo is written using the `Python Twitter `_ 5 | wrapper, and through `Coo.api` you gain access to all of his models: 6 | 7 | .. code-block:: python 8 | 9 | # get your followers 10 | followers = at.api.GetFollowers() 11 | 12 | # get your direct messages 13 | d_messages = at.api.GetDirectMessages() 14 | 15 | # favorited tweets 16 | favorites = at.api.GetFavorites() 17 | 18 | # mentions 19 | mentions = at.api.GetMentions() 20 | 21 | # retweets 22 | retweets = at.api.GetRetweets() 23 | 24 | And a lot more. If you are interested, check their `documentation `_. 25 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "A configurable sidebar-enabled Sphinx theme" 4 | name = "alabaster" 5 | optional = false 6 | python-versions = "*" 7 | version = "0.7.12" 8 | 9 | [[package]] 10 | category = "dev" 11 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 12 | name = "appdirs" 13 | optional = false 14 | python-versions = "*" 15 | version = "1.4.3" 16 | 17 | [[package]] 18 | category = "dev" 19 | description = "Atomic file writes." 20 | name = "atomicwrites" 21 | optional = false 22 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 23 | version = "1.3.0" 24 | 25 | [[package]] 26 | category = "dev" 27 | description = "Classes Without Boilerplate" 28 | name = "attrs" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 31 | version = "19.3.0" 32 | 33 | [package.extras] 34 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 35 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 36 | docs = ["sphinx", "zope.interface"] 37 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 38 | 39 | [[package]] 40 | category = "dev" 41 | description = "Internationalization utilities" 42 | name = "babel" 43 | optional = false 44 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 45 | version = "2.8.0" 46 | 47 | [package.dependencies] 48 | pytz = ">=2015.7" 49 | 50 | [[package]] 51 | category = "dev" 52 | description = "The uncompromising code formatter." 53 | name = "black" 54 | optional = false 55 | python-versions = ">=3.6" 56 | version = "19.10b0" 57 | 58 | [package.dependencies] 59 | appdirs = "*" 60 | attrs = ">=18.1.0" 61 | click = ">=6.5" 62 | pathspec = ">=0.6,<1" 63 | regex = "*" 64 | toml = ">=0.9.4" 65 | typed-ast = ">=1.4.0" 66 | 67 | [package.extras] 68 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 69 | 70 | [[package]] 71 | category = "main" 72 | description = "Python package for providing Mozilla's CA Bundle." 73 | name = "certifi" 74 | optional = false 75 | python-versions = "*" 76 | version = "2019.11.28" 77 | 78 | [[package]] 79 | category = "main" 80 | description = "Universal encoding detector for Python 2 and 3" 81 | name = "chardet" 82 | optional = false 83 | python-versions = "*" 84 | version = "3.0.4" 85 | 86 | [[package]] 87 | category = "dev" 88 | description = "Composable command line interface toolkit" 89 | name = "click" 90 | optional = false 91 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 92 | version = "7.1.1" 93 | 94 | [[package]] 95 | category = "dev" 96 | description = "Cross-platform colored terminal text." 97 | marker = "sys_platform == \"win32\" and python_version != \"3.4\" or sys_platform == \"win32\"" 98 | name = "colorama" 99 | optional = false 100 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 101 | version = "0.4.3" 102 | 103 | [[package]] 104 | category = "dev" 105 | description = "Docutils -- Python Documentation Utilities" 106 | name = "docutils" 107 | optional = false 108 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 109 | version = "0.16" 110 | 111 | [[package]] 112 | category = "dev" 113 | description = "Discover and load entry points from installed packages." 114 | name = "entrypoints" 115 | optional = false 116 | python-versions = ">=2.7" 117 | version = "0.3" 118 | 119 | [[package]] 120 | category = "dev" 121 | description = "the modular source code checker: pep8, pyflakes and co" 122 | name = "flake8" 123 | optional = false 124 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 125 | version = "3.7.9" 126 | 127 | [package.dependencies] 128 | entrypoints = ">=0.3.0,<0.4.0" 129 | mccabe = ">=0.6.0,<0.7.0" 130 | pycodestyle = ">=2.5.0,<2.6.0" 131 | pyflakes = ">=2.1.0,<2.2.0" 132 | 133 | [[package]] 134 | category = "main" 135 | description = "Clean single-source support for Python 3 and 2" 136 | name = "future" 137 | optional = false 138 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 139 | version = "0.18.2" 140 | 141 | [[package]] 142 | category = "main" 143 | description = "Internationalized Domain Names in Applications (IDNA)" 144 | name = "idna" 145 | optional = false 146 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 147 | version = "2.9" 148 | 149 | [[package]] 150 | category = "dev" 151 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 152 | name = "imagesize" 153 | optional = false 154 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 155 | version = "1.2.0" 156 | 157 | [[package]] 158 | category = "dev" 159 | description = "Read metadata from Python packages" 160 | marker = "python_version < \"3.8\"" 161 | name = "importlib-metadata" 162 | optional = false 163 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 164 | version = "1.6.0" 165 | 166 | [package.dependencies] 167 | zipp = ">=0.5" 168 | 169 | [package.extras] 170 | docs = ["sphinx", "rst.linker"] 171 | testing = ["packaging", "importlib-resources"] 172 | 173 | [[package]] 174 | category = "dev" 175 | description = "A very fast and expressive template engine." 176 | name = "jinja2" 177 | optional = false 178 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 179 | version = "2.11.1" 180 | 181 | [package.dependencies] 182 | MarkupSafe = ">=0.23" 183 | 184 | [package.extras] 185 | i18n = ["Babel (>=0.8)"] 186 | 187 | [[package]] 188 | category = "dev" 189 | description = "Safely add untrusted strings to HTML/XML markup." 190 | name = "markupsafe" 191 | optional = false 192 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 193 | version = "1.1.1" 194 | 195 | [[package]] 196 | category = "dev" 197 | description = "McCabe checker, plugin for flake8" 198 | name = "mccabe" 199 | optional = false 200 | python-versions = "*" 201 | version = "0.6.1" 202 | 203 | [[package]] 204 | category = "dev" 205 | description = "More routines for operating on iterables, beyond itertools" 206 | marker = "python_version > \"2.7\"" 207 | name = "more-itertools" 208 | optional = false 209 | python-versions = ">=3.5" 210 | version = "8.2.0" 211 | 212 | [[package]] 213 | category = "main" 214 | description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" 215 | name = "oauthlib" 216 | optional = false 217 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 218 | version = "3.1.0" 219 | 220 | [package.extras] 221 | rsa = ["cryptography"] 222 | signals = ["blinker"] 223 | signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] 224 | 225 | [[package]] 226 | category = "dev" 227 | description = "Core utilities for Python packages" 228 | name = "packaging" 229 | optional = false 230 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 231 | version = "20.3" 232 | 233 | [package.dependencies] 234 | pyparsing = ">=2.0.2" 235 | six = "*" 236 | 237 | [[package]] 238 | category = "dev" 239 | description = "Utility library for gitignore style pattern matching of file paths." 240 | name = "pathspec" 241 | optional = false 242 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 243 | version = "0.7.0" 244 | 245 | [[package]] 246 | category = "main" 247 | description = "Python datetimes made easy" 248 | name = "pendulum" 249 | optional = false 250 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 251 | version = "2.1.0" 252 | 253 | [package.dependencies] 254 | python-dateutil = ">=2.6,<3.0" 255 | pytzdata = ">=2018.3" 256 | 257 | [[package]] 258 | category = "dev" 259 | description = "plugin and hook calling mechanisms for python" 260 | name = "pluggy" 261 | optional = false 262 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 263 | version = "0.13.1" 264 | 265 | [package.dependencies] 266 | [package.dependencies.importlib-metadata] 267 | python = "<3.8" 268 | version = ">=0.12" 269 | 270 | [package.extras] 271 | dev = ["pre-commit", "tox"] 272 | 273 | [[package]] 274 | category = "dev" 275 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 276 | name = "py" 277 | optional = false 278 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 279 | version = "1.8.1" 280 | 281 | [[package]] 282 | category = "dev" 283 | description = "Python style guide checker" 284 | name = "pycodestyle" 285 | optional = false 286 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 287 | version = "2.5.0" 288 | 289 | [[package]] 290 | category = "dev" 291 | description = "passive checker of Python programs" 292 | name = "pyflakes" 293 | optional = false 294 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 295 | version = "2.1.1" 296 | 297 | [[package]] 298 | category = "dev" 299 | description = "Pygments is a syntax highlighting package written in Python." 300 | name = "pygments" 301 | optional = false 302 | python-versions = ">=3.5" 303 | version = "2.6.1" 304 | 305 | [[package]] 306 | category = "dev" 307 | description = "Python parsing module" 308 | name = "pyparsing" 309 | optional = false 310 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 311 | version = "2.4.6" 312 | 313 | [[package]] 314 | category = "dev" 315 | description = "pytest: simple powerful testing with Python" 316 | name = "pytest" 317 | optional = false 318 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 319 | version = "4.6.9" 320 | 321 | [package.dependencies] 322 | atomicwrites = ">=1.0" 323 | attrs = ">=17.4.0" 324 | packaging = "*" 325 | pluggy = ">=0.12,<1.0" 326 | py = ">=1.5.0" 327 | six = ">=1.10.0" 328 | wcwidth = "*" 329 | 330 | [package.dependencies.colorama] 331 | python = "<3.4.0 || >=3.5.0" 332 | version = "*" 333 | 334 | [package.dependencies.importlib-metadata] 335 | python = "<3.8" 336 | version = ">=0.12" 337 | 338 | [package.dependencies.more-itertools] 339 | python = ">=2.8" 340 | version = ">=4.0.0" 341 | 342 | [package.extras] 343 | testing = ["argcomplete", "hypothesis (>=3.56)", "nose", "requests", "mock"] 344 | 345 | [[package]] 346 | category = "main" 347 | description = "Extensions to the standard Python datetime module" 348 | name = "python-dateutil" 349 | optional = false 350 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 351 | version = "2.8.1" 352 | 353 | [package.dependencies] 354 | six = ">=1.5" 355 | 356 | [[package]] 357 | category = "main" 358 | description = "A Python wrapper around the Twitter API" 359 | name = "python-twitter" 360 | optional = false 361 | python-versions = "*" 362 | version = "3.5" 363 | 364 | [package.dependencies] 365 | future = "*" 366 | requests = "*" 367 | requests-oauthlib = "*" 368 | 369 | [[package]] 370 | category = "dev" 371 | description = "World timezone definitions, modern and historical" 372 | name = "pytz" 373 | optional = false 374 | python-versions = "*" 375 | version = "2019.3" 376 | 377 | [[package]] 378 | category = "main" 379 | description = "The Olson timezone database for Python." 380 | name = "pytzdata" 381 | optional = false 382 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 383 | version = "2019.3" 384 | 385 | [[package]] 386 | category = "dev" 387 | description = "Alternative regular expression module, to replace re." 388 | name = "regex" 389 | optional = false 390 | python-versions = "*" 391 | version = "2020.2.20" 392 | 393 | [[package]] 394 | category = "main" 395 | description = "Python HTTP for Humans." 396 | name = "requests" 397 | optional = false 398 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 399 | version = "2.23.0" 400 | 401 | [package.dependencies] 402 | certifi = ">=2017.4.17" 403 | chardet = ">=3.0.2,<4" 404 | idna = ">=2.5,<3" 405 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" 406 | 407 | [package.extras] 408 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 409 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] 410 | 411 | [[package]] 412 | category = "main" 413 | description = "OAuthlib authentication support for Requests." 414 | name = "requests-oauthlib" 415 | optional = false 416 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 417 | version = "1.3.0" 418 | 419 | [package.dependencies] 420 | oauthlib = ">=3.0.0" 421 | requests = ">=2.0.0" 422 | 423 | [package.extras] 424 | rsa = ["oauthlib (>=3.0.0)"] 425 | 426 | [[package]] 427 | category = "main" 428 | description = "Python 2 and 3 compatibility utilities" 429 | name = "six" 430 | optional = false 431 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 432 | version = "1.14.0" 433 | 434 | [[package]] 435 | category = "dev" 436 | description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." 437 | name = "snowballstemmer" 438 | optional = false 439 | python-versions = "*" 440 | version = "2.0.0" 441 | 442 | [[package]] 443 | category = "dev" 444 | description = "Python documentation generator" 445 | name = "sphinx" 446 | optional = false 447 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 448 | version = "1.8.5" 449 | 450 | [package.dependencies] 451 | Jinja2 = ">=2.3" 452 | Pygments = ">=2.0" 453 | alabaster = ">=0.7,<0.8" 454 | babel = ">=1.3,<2.0 || >2.0" 455 | colorama = ">=0.3.5" 456 | docutils = ">=0.11" 457 | imagesize = "*" 458 | packaging = "*" 459 | requests = ">=2.0.0" 460 | setuptools = "*" 461 | six = ">=1.5" 462 | snowballstemmer = ">=1.1" 463 | sphinxcontrib-websupport = "*" 464 | 465 | [package.extras] 466 | test = ["mock", "pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "enum34", "mypy", "typed-ast"] 467 | websupport = ["sqlalchemy (>=0.9)", "whoosh (>=2.0)"] 468 | 469 | [[package]] 470 | category = "dev" 471 | description = "Sphinx API for Web Apps" 472 | name = "sphinxcontrib-websupport" 473 | optional = false 474 | python-versions = ">=3.5" 475 | version = "1.2.1" 476 | 477 | [package.extras] 478 | lint = ["flake8"] 479 | test = ["pytest", "sqlalchemy", "whoosh", "sphinx"] 480 | 481 | [[package]] 482 | category = "dev" 483 | description = "Python Library for Tom's Obvious, Minimal Language" 484 | name = "toml" 485 | optional = false 486 | python-versions = "*" 487 | version = "0.10.0" 488 | 489 | [[package]] 490 | category = "dev" 491 | description = "a fork of Python 2 and 3 ast modules with type comment support" 492 | name = "typed-ast" 493 | optional = false 494 | python-versions = "*" 495 | version = "1.4.1" 496 | 497 | [[package]] 498 | category = "main" 499 | description = "HTTP library with thread-safe connection pooling, file post, and more." 500 | name = "urllib3" 501 | optional = false 502 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 503 | version = "1.25.8" 504 | 505 | [package.extras] 506 | brotli = ["brotlipy (>=0.6.0)"] 507 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 508 | socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] 509 | 510 | [[package]] 511 | category = "dev" 512 | description = "Measures number of Terminal column cells of wide-character codes" 513 | name = "wcwidth" 514 | optional = false 515 | python-versions = "*" 516 | version = "0.1.9" 517 | 518 | [[package]] 519 | category = "dev" 520 | description = "Backport of pathlib-compatible object wrapper for zip files" 521 | marker = "python_version < \"3.8\"" 522 | name = "zipp" 523 | optional = false 524 | python-versions = ">=3.6" 525 | version = "3.1.0" 526 | 527 | [package.extras] 528 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 529 | testing = ["jaraco.itertools", "func-timeout"] 530 | 531 | [metadata] 532 | content-hash = "967efad63d76f273668e8c13a20d219227f90be1f4a908041b46029de92b1ef1" 533 | python-versions = "^3.7" 534 | 535 | [metadata.files] 536 | alabaster = [ 537 | {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, 538 | {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, 539 | ] 540 | appdirs = [ 541 | {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, 542 | {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, 543 | ] 544 | atomicwrites = [ 545 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 546 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 547 | ] 548 | attrs = [ 549 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 550 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 551 | ] 552 | babel = [ 553 | {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, 554 | {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, 555 | ] 556 | black = [ 557 | {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, 558 | {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, 559 | ] 560 | certifi = [ 561 | {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, 562 | {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, 563 | ] 564 | chardet = [ 565 | {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, 566 | {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, 567 | ] 568 | click = [ 569 | {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"}, 570 | {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"}, 571 | ] 572 | colorama = [ 573 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 574 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 575 | ] 576 | docutils = [ 577 | {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, 578 | {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, 579 | ] 580 | entrypoints = [ 581 | {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, 582 | {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, 583 | ] 584 | flake8 = [ 585 | {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, 586 | {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, 587 | ] 588 | future = [ 589 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 590 | ] 591 | idna = [ 592 | {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, 593 | {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, 594 | ] 595 | imagesize = [ 596 | {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, 597 | {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, 598 | ] 599 | importlib-metadata = [ 600 | {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, 601 | {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, 602 | ] 603 | jinja2 = [ 604 | {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"}, 605 | {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"}, 606 | ] 607 | markupsafe = [ 608 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 609 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 610 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 611 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 612 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 613 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 614 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 615 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 616 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 617 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 618 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 619 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 620 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 621 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 622 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 623 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 624 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 625 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 626 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 627 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 628 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 629 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 630 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 631 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 632 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 633 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 634 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 635 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 636 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 637 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 638 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 639 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 640 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 641 | ] 642 | mccabe = [ 643 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 644 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 645 | ] 646 | more-itertools = [ 647 | {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, 648 | {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, 649 | ] 650 | oauthlib = [ 651 | {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, 652 | {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, 653 | ] 654 | packaging = [ 655 | {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, 656 | {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, 657 | ] 658 | pathspec = [ 659 | {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, 660 | {file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"}, 661 | ] 662 | pendulum = [ 663 | {file = "pendulum-2.1.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:9eda38ff65b1f297d860d3f562480e048673fb4b81fdd5c8c55decb519b97ed2"}, 664 | {file = "pendulum-2.1.0-cp27-cp27m-win_amd64.whl", hash = "sha256:70007aebc4494163f8705909a1996ce21ab853801b57fba4c2dd53c3df5c38f0"}, 665 | {file = "pendulum-2.1.0-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:575934b65b298eeb99c5a5b1673c945fc5c99e2b56caff772a91bc4b1eba7b82"}, 666 | {file = "pendulum-2.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:d42d1e870541eeaf3fe0500aac0c76a85bd4bd53ebed74f9a7daf8f01ac77374"}, 667 | {file = "pendulum-2.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff7f3420de0c0cf21c1fc813d581fcfa4a1fb6d87f09485880b3e1204eb9cdd7"}, 668 | {file = "pendulum-2.1.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ac3c6a992beeb4c9bd90c317a1bb2a6cba159b49a49b6dd3c86b5bacb86f3d50"}, 669 | {file = "pendulum-2.1.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:75a62e3f98499283fafe8ef4b44f81052e84825b00a0b64609dd8a06985382b9"}, 670 | {file = "pendulum-2.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a79a72a7fd1092a7c69ddd8580a0be5365ded40c9f9c865623c7665742e3b888"}, 671 | {file = "pendulum-2.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:701127e1f0ff7c253cc0c07f29becc5f9210547914e0bbe59ffd9fa064d7c3c8"}, 672 | {file = "pendulum-2.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:816e01dcb0ba4ffcf2ceaafe4d644174fea680361e909f6f8ba0a4fdb2ccae24"}, 673 | {file = "pendulum-2.1.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:edd00e6b43698762e10bfda508cc9c06bad88c0703a9b37e412aec1189e06e23"}, 674 | {file = "pendulum-2.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4420e058110740a8193fb0709350dfc6ac790a99c345fc4e92e24df0f834ddcb"}, 675 | {file = "pendulum-2.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa560bd39d94f3889646422f1e65b8dfd025bf6288d43e5c2e31d4f972aaf2e4"}, 676 | {file = "pendulum-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2788945a0111d5325fd27ae3e3b18b741e440d20bdb7d4ea22fce7c9a4fbbf40"}, 677 | {file = "pendulum-2.1.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:eb7e349bb2d1b2b418d094e2179d6768561e8242fd8cb640b5aaba735f3e91d1"}, 678 | {file = "pendulum-2.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6cf0f876cd088ee1578266f4231121376747aa90c3ed3b8e212a8344a9920061"}, 679 | {file = "pendulum-2.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:aa13ddea12fd871d3191f633f08090b91ea2e80fb0ed50a7a149add7f680b12d"}, 680 | {file = "pendulum-2.1.0-cp38-cp38m-win_amd64.whl", hash = "sha256:0cbbd4f30c69a283690d9ed8e58e44a990e067e59ee05b5ef55d022b38659aeb"}, 681 | {file = "pendulum-2.1.0.tar.gz", hash = "sha256:093cab342e10516660e64b935a6da1a043e0286de36cc229fb48471415981ffe"}, 682 | ] 683 | pluggy = [ 684 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 685 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 686 | ] 687 | py = [ 688 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 689 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 690 | ] 691 | pycodestyle = [ 692 | {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, 693 | {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, 694 | ] 695 | pyflakes = [ 696 | {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, 697 | {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, 698 | ] 699 | pygments = [ 700 | {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, 701 | {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, 702 | ] 703 | pyparsing = [ 704 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 705 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 706 | ] 707 | pytest = [ 708 | {file = "pytest-4.6.9-py2.py3-none-any.whl", hash = "sha256:c77a5f30a90e0ce24db9eaa14ddfd38d4afb5ea159309bdd2dae55b931bc9324"}, 709 | {file = "pytest-4.6.9.tar.gz", hash = "sha256:19e8f75eac01dd3f211edd465b39efbcbdc8fc5f7866d7dd49fedb30d8adf339"}, 710 | ] 711 | python-dateutil = [ 712 | {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, 713 | {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, 714 | ] 715 | python-twitter = [ 716 | {file = "python-twitter-3.5.tar.gz", hash = "sha256:45855742f1095aa0c8c57b2983eee3b6b7f527462b50a2fa8437a8b398544d90"}, 717 | {file = "python_twitter-3.5-py2.py3-none-any.whl", hash = "sha256:4a420a6cb6ee9d0c8da457c8a8573f709c2ff2e1a7542e2d38807ebbfe8ebd1d"}, 718 | ] 719 | pytz = [ 720 | {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, 721 | {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, 722 | ] 723 | pytzdata = [ 724 | {file = "pytzdata-2019.3-py2.py3-none-any.whl", hash = "sha256:84c52b9a47d097fcd483f047a544979de6c3a86e94c845e3569e9f8acd0fa071"}, 725 | {file = "pytzdata-2019.3.tar.gz", hash = "sha256:fac06f7cdfa903188dc4848c655e4adaee67ee0f2fe08e7daf815cf2a761ee5e"}, 726 | ] 727 | regex = [ 728 | {file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"}, 729 | {file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"}, 730 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400"}, 731 | {file = "regex-2020.2.20-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0"}, 732 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc"}, 733 | {file = "regex-2020.2.20-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0"}, 734 | {file = "regex-2020.2.20-cp36-cp36m-win32.whl", hash = "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69"}, 735 | {file = "regex-2020.2.20-cp36-cp36m-win_amd64.whl", hash = "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b"}, 736 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e"}, 737 | {file = "regex-2020.2.20-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242"}, 738 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce"}, 739 | {file = "regex-2020.2.20-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab"}, 740 | {file = "regex-2020.2.20-cp37-cp37m-win32.whl", hash = "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431"}, 741 | {file = "regex-2020.2.20-cp37-cp37m-win_amd64.whl", hash = "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1"}, 742 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_i686.whl", hash = "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045"}, 743 | {file = "regex-2020.2.20-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26"}, 744 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2"}, 745 | {file = "regex-2020.2.20-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70"}, 746 | {file = "regex-2020.2.20-cp38-cp38-win32.whl", hash = "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d"}, 747 | {file = "regex-2020.2.20-cp38-cp38-win_amd64.whl", hash = "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa"}, 748 | {file = "regex-2020.2.20.tar.gz", hash = "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5"}, 749 | ] 750 | requests = [ 751 | {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, 752 | {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, 753 | ] 754 | requests-oauthlib = [ 755 | {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, 756 | {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, 757 | {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, 758 | ] 759 | six = [ 760 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 761 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 762 | ] 763 | snowballstemmer = [ 764 | {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, 765 | {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, 766 | ] 767 | sphinx = [ 768 | {file = "Sphinx-1.8.5-py2.py3-none-any.whl", hash = "sha256:9f3e17c64b34afc653d7c5ec95766e03043cc6d80b0de224f59b6b6e19d37c3c"}, 769 | {file = "Sphinx-1.8.5.tar.gz", hash = "sha256:c7658aab75c920288a8cf6f09f244c6cfdae30d82d803ac1634d9f223a80ca08"}, 770 | ] 771 | sphinxcontrib-websupport = [ 772 | {file = "sphinxcontrib-websupport-1.2.1.tar.gz", hash = "sha256:545f5da4bd7757e82b8a23ce3af9500c6ffeedbcb13429fca436ad1e80bd10cf"}, 773 | {file = "sphinxcontrib_websupport-1.2.1-py2.py3-none-any.whl", hash = "sha256:69364896eae5d1145d82b6ee09f66d597099ef8069615e2888921ec48005470f"}, 774 | ] 775 | toml = [ 776 | {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, 777 | {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, 778 | {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, 779 | ] 780 | typed-ast = [ 781 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, 782 | {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, 783 | {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, 784 | {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, 785 | {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, 786 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, 787 | {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, 788 | {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, 789 | {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, 790 | {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, 791 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, 792 | {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, 793 | {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, 794 | {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, 795 | {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, 796 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, 797 | {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, 798 | {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, 799 | {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, 800 | {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, 801 | {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, 802 | ] 803 | urllib3 = [ 804 | {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, 805 | {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, 806 | ] 807 | wcwidth = [ 808 | {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, 809 | {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, 810 | ] 811 | zipp = [ 812 | {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, 813 | {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, 814 | ] 815 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "coo" 3 | version = "0.1.2" 4 | description = "schedule Twitter updates with easy" 5 | authors = ["wilfredinni"] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.7" 10 | pendulum = "^2.0" 11 | python-twitter = "^3.5" 12 | 13 | [tool.poetry.dev-dependencies] 14 | black = { version = "*", allow-prereleases = true } 15 | flake8 = "^3.6" 16 | pytest = "^4.0" 17 | sphinx = "^1.8" 18 | 19 | [build-system] 20 | requires = ["poetry>=0.12"] 21 | build-backend = "poetry.masonry.api" 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pendulum 2 | python-twitter -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import re 3 | 4 | with open("coo/__init__.py", "r") as file: 5 | regex_version = r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]' 6 | version = re.search(regex_version, file.read(), re.MULTILINE).group(1) 7 | 8 | with open("README_P.rst") as readme_file: 9 | readme = readme_file.read() 10 | 11 | 12 | requirements = ["python-twitter", "pendulum"] 13 | 14 | test_requirements = ["pytest", "flake8", "flake8-mypy", "black"] 15 | 16 | setup( 17 | author="Carlos Montecinos Geisse", 18 | author_email="carlos.w.montecinos@gmail.com", 19 | classifiers=[ 20 | "Development Status :: 2 - Pre-Alpha", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Natural Language :: English", 24 | "Programming Language :: Python :: 3.6", 25 | "Programming Language :: Python :: 3.7", 26 | ], 27 | description="Schedule Twitter Updates with Easy", 28 | install_requires=requirements, 29 | license="MIT License", 30 | long_description=readme, 31 | include_package_data=True, 32 | keywords="coo", 33 | name="coo", 34 | packages=find_packages(include=["coo"]), 35 | test_suite="tests", 36 | tests_require=test_requirements, 37 | url="https://github.com/wilfredinni/coo", 38 | version=version, 39 | zip_safe=False, 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilfredinni/coo/e6dbfc11aadfdde27c6edbd0343adb54b849a5fc/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_coo.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pathlib import Path 3 | 4 | import pytest 5 | from twitter.error import TwitterError 6 | 7 | from coo import Coo 8 | from coo._exceptions import TweetTypeError, ScheduleError 9 | 10 | 11 | # Mock Update list 12 | m_updates = ["mock1", "mock2", "mock3", "mock4", "mock5"] 13 | 14 | 15 | def test_input_types(): 16 | with pytest.raises(TypeError): 17 | Coo([1], [1], [1], [1]) 18 | 19 | with pytest.raises(TypeError): 20 | Coo({1: 5}, {1: 5}, {1: 5}, {1: 5}) 21 | 22 | with pytest.raises(TypeError): 23 | Coo(1, 2, 3, 4) 24 | 25 | # correct construction. no error 26 | Coo("mock", "mock", "mock", "mock") 27 | 28 | 29 | @pytest.fixture 30 | def coo_preview_instance(): 31 | yield Coo("mock", "mock", "mock", "mock", preview=True) 32 | 33 | 34 | @pytest.fixture 35 | def coo_mock_instance(): 36 | yield Coo("mock1", "mock2", "mock3", "mock4") 37 | 38 | 39 | # API 40 | def test_wrong_credentials_TwitterError(coo_mock_instance): 41 | with pytest.raises(TwitterError): 42 | coo_mock_instance.verify 43 | 44 | 45 | # TWEET 46 | @pytest.mark.parametrize( 47 | "updates, delay, interval, template, time_zone", 48 | [ 49 | (m_updates, None, None, None, None), 50 | # One None 51 | (m_updates, None, "now", "$message", "local"), 52 | (m_updates, "now", None, "$message", "local"), 53 | (m_updates, "now", "now", None, "local"), 54 | (m_updates, "now", "now", "$message", None), 55 | # Two None 56 | (m_updates, None, None, "$message", "local"), 57 | (m_updates, "now", None, None, "local"), 58 | (m_updates, "now", "now", None, None), 59 | (m_updates, None, "now", "$message", None), 60 | # _delay 61 | (m_updates, "now", None, None, None), 62 | (m_updates, 0, None, None, None), 63 | # _interval 64 | (m_updates, None, "now", None, None), 65 | (m_updates, None, 0, None, None), 66 | # Template 67 | (m_updates, None, None, "$message", None), 68 | # Time zone 69 | (m_updates, None, None, None, "local"), 70 | (m_updates, None, None, None, "America/Santiago "), 71 | ], 72 | ) 73 | def test_tweet(coo_preview_instance, updates, delay, interval, template, time_zone): 74 | coo_preview_instance.tweet(updates, delay, interval, template, time_zone) 75 | 76 | 77 | @pytest.mark.parametrize( 78 | "tz", 79 | [ 80 | ("Canada/Yukon"), 81 | ("Brazil/Acre"), 82 | ("Australia/Tasmania"), 83 | ("America/Santiago"), 84 | ("America/Detroit"), 85 | ("Asia/Atyrau"), 86 | ], 87 | ) 88 | def test_tweet_time_zone(coo_preview_instance, tz): 89 | coo_preview_instance.tweet(["mock"], time_zone=tz) 90 | assert coo_preview_instance.time_zone == tz 91 | 92 | 93 | def test_tweet_random(coo_preview_instance): 94 | updates = ["mock1", "mock2", "mock3", "mock4", "mock5"] 95 | coo_preview_instance.tweet(m_updates, aleatory=True) 96 | assert updates != m_updates 97 | 98 | 99 | def test_tweet_media_update(coo_preview_instance): 100 | coo_preview_instance.tweet(["mock"], media="../coo.png") 101 | assert coo_preview_instance.media == Path("../coo.png") 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "updates", 106 | [ 107 | # update is not a instance of list: 108 | ((1, 2, 3)), 109 | ({1, 2, 3}), 110 | (123), 111 | ("string"), 112 | # The instances 'in' the list are no strings: 113 | ([(1, 2, 3)]), 114 | ([{1, 2, 3}]), 115 | ([[1, 2, 3]]), 116 | ([1, 2, 3]), 117 | ], 118 | ) 119 | def test_tweet_TweetTypeError(coo_preview_instance, updates): 120 | with pytest.raises(TweetTypeError): 121 | coo_preview_instance.tweet(updates) 122 | 123 | 124 | def test_tweet_media_FileNotFoundError(coo_mock_instance): 125 | with pytest.raises(FileNotFoundError): 126 | coo_mock_instance.tweet(["mock"], media="coo_.png") 127 | 128 | 129 | def test_tweet_media_TwitterError(coo_mock_instance): 130 | with pytest.raises(TwitterError): 131 | coo_mock_instance.tweet(["mock"], media="coo.png") 132 | 133 | 134 | def test_tweet_none_media_TwitterError(coo_mock_instance): 135 | with pytest.raises(TwitterError): 136 | coo_mock_instance.tweet(["mock"], media=None) 137 | 138 | 139 | # SCHEDULE 140 | def test_schedule_time_zone_media(coo_preview_instance): 141 | updates = [ 142 | ("now", "template", "update"), 143 | (0, "template", "update"), 144 | ("now", None, "update"), 145 | (0, None, "update"), 146 | (0, None, "update", "../coo.png"), 147 | ] 148 | coo_preview_instance.loop = asyncio.new_event_loop() 149 | coo_preview_instance.schedule(updates, time_zone="Canada/Yukon", media="../coo.png") 150 | assert coo_preview_instance.time_zone == "Canada/Yukon" 151 | assert coo_preview_instance.media == Path("../coo.png") 152 | assert coo_preview_instance.global_media == Path("../coo.png") 153 | 154 | 155 | @pytest.mark.parametrize( 156 | "updates", 157 | [ 158 | ([["update1", "update2"]]), 159 | ([{"update1", "update2"}]), 160 | (["update1", "update2"]), 161 | ([123, 456, 789]), 162 | # len tuple 163 | ([("now")]), 164 | ], 165 | ) 166 | def test_schedule_ScheduleError(coo_preview_instance, updates): 167 | with pytest.raises(ScheduleError): 168 | coo_preview_instance.schedule(updates) 169 | 170 | 171 | # STR UPDATE 172 | @pytest.mark.parametrize( 173 | "update, template", [("My Twitter Update", None), ("My Twitter Update", "$message")] 174 | ) 175 | def test__str_update(coo_preview_instance, update, template): 176 | coo_preview_instance._str_update(update, template) 177 | 178 | 179 | # _delay 180 | @pytest.mark.parametrize("delay", [(0), ("now")]) 181 | def test_delay(coo_preview_instance, delay): 182 | coo_preview_instance._delay(delay) 183 | 184 | 185 | # _interval 186 | @pytest.mark.parametrize("_interval", [(0), ("now")]) 187 | def test__interval(coo_preview_instance, _interval): 188 | coo_preview_instance._interval(_interval) 189 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from coo._utils import parse_time, parse_or_get, zzz, tweet_template, TIME_DICT 4 | from coo._exceptions import ScheduleError, TemplateError 5 | 6 | 7 | # DICTIONARIES 8 | @pytest.mark.parametrize( 9 | "time_delay, int_value", 10 | [ 11 | ("now", 0), 12 | ("half_hour", 1800), 13 | ("one_hour", 3600), 14 | ("two_hours", 7200), 15 | ("four_hours", 14400), 16 | ("six_hours", 21600), 17 | ("eight_hours", 28800), 18 | ("ten_hours", 36000), 19 | ("twelve_hours", 43200), 20 | ("fourteen_hours", 50400), 21 | ("sixteen_hours", 57600), 22 | ("eighteen_hours", 64800), 23 | ("twenty_hours", 72000), 24 | ("twenty_two_hours", 79200), 25 | ("one_day", 86400), 26 | ("two_days", 172800), 27 | ("three_days", 259200), 28 | ("four_days", 345600), 29 | ("five_days", 432000), 30 | ("six_days", 518400), 31 | ("one_week", 604800), 32 | ], 33 | ) 34 | def test_TIME_DICT(time_delay, int_value): 35 | assert TIME_DICT.get(time_delay) == int_value 36 | 37 | 38 | # PARSE TIME 39 | @pytest.mark.parametrize( 40 | "date_time, time_zone", 41 | [("2040-10-28 18:46", "America/Santiago"), ("2040-10-28", "America/Santiago")], 42 | ) 43 | def test_parse_time(date_time, time_zone): 44 | secs = parse_time(date_time, time_zone) 45 | assert isinstance(secs, int) 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "schedule_time, time_zone", 50 | [("2015-10-28 18:46", "America/Santiago"), ("2002-10-28", "America/Santiago")], 51 | ) 52 | def test_parse_time_ScheduleError(schedule_time, time_zone): 53 | with pytest.raises(ScheduleError): 54 | parse_time(schedule_time, time_zone) 55 | 56 | 57 | # PARSE OR GET 58 | @pytest.mark.parametrize( 59 | "schedule_time, time_zone", 60 | [ 61 | (20, None), 62 | ("now", None), 63 | ("2040-10-28", "America/Santiago"), 64 | ("2040-10-28 18:46", "America/Santiago"), 65 | ], 66 | ) 67 | def test_parse_or_get(schedule_time, time_zone): 68 | seconds = parse_or_get(schedule_time, time_zone) 69 | assert isinstance(seconds, int) 70 | 71 | 72 | @pytest.mark.parametrize( 73 | "schedule_time, time_zone", 74 | [ 75 | ("wrong_delay_time", "America/Santiago"), 76 | ("wrong_delay_time", "America/Santiago"), 77 | ], 78 | ) 79 | def test_parse_or_get_TypeError(schedule_time, time_zone): 80 | with pytest.raises(TypeError): 81 | parse_or_get(schedule_time, time_zone) 82 | 83 | 84 | # ZZZ 85 | @pytest.mark.parametrize("sleep_time, time_zone", [(0, None), ("now", None)]) 86 | def test_zzz_INT(sleep_time, time_zone): 87 | zzz(sleep_time, time_zone) 88 | 89 | 90 | # TEMPLATE 91 | def test_tweet_template(): 92 | assert isinstance(tweet_template("msg", "$message"), str) 93 | 94 | 95 | @pytest.mark.parametrize( 96 | "update, template", 97 | [ 98 | ("msg", None), 99 | ("msg", (1, 2, 3)), 100 | ("msg", [1, 2, 3]), 101 | ("msg", {1, 2, 3}), 102 | ("msg", {1: 2}), 103 | ], 104 | ) 105 | def test_tweet_template_TemplateError(update, template): 106 | with pytest.raises(TemplateError): 107 | tweet_template(update, template) 108 | 109 | 110 | def test_message_template_TemplateError(): 111 | with pytest.raises(TemplateError): 112 | tweet_template("update", "template") 113 | --------------------------------------------------------------------------------