├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.md ├── conftest.py ├── gitcoin ├── __init__.py ├── client.py └── validation.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_dry_run.py └── test_live.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .pytest_cache/ 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | pip-selfcheck.json 30 | pyvenv.cfg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | .DS_Store 111 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: true 3 | dist: xenial 4 | cache: pip 5 | deploy: 6 | provider: pypi 7 | user: gitcoin 8 | password: 9 | secure: WkOcP5fbXeiA5jtLAm8Ab5d0+Ycdz/0sQ9qGNsSN0h8hl1aZ4oRXqY50WXfwZcEgnIEx5q13RGO68dqWLqNpAUw/CDIKfALJ13O6rsRodjP3cT43XDWGbOKDlQsFEp58EsQ3puzI+1NVewAjijsZtullAKQcv+h5jS5im1lUEl7tyyY47GQyj+8uWGaA4HZTMkstCAu6UMNoYDbgwjyaA+u/iqYd/8sCorahSTu5HugcDKQP7F4epMEWJS5GDpoPBMSEQlEQySs2GwAmAKeUBKBs6uU7DHOAS1jPyRe51tzZw8IPX5Ya11FdBzor5F4RDwnsRMrq1ODmwknU2FTqde111fgIzM6cMqEAoPQhw0H6yuuxsr8m9DPGX1Awh3WLBOGq4tQZLtHDd/vsU8LdYwXJ6kCNhSJH/PPAYkwGrdl0O2E2QAWO0jIX/rL1Lo4kx8Zl9ZgAeH1maFF9bFhaTZ31wMBWIXxfBnVbwuGK7cWmLEf9Bslev5fg99jd5PHltgPcsW21AXiWNjsivSmBCStOXAKgldT2oPjvl8ptrXKzePReKC0d13PPZ/YYMlZhjdPcwTNkUlajnsvdnm+BXuvBkUe08COT5J7oJfmagAFwlc/ddSr7OA/+fIzLCsgCAJ3pCEyo/dql35Tfoov0kRdWxKQ4BvvlJJDwcfWKiWM= 10 | distributions: sdist bdist_wheel 11 | on: 12 | tags: true 13 | addons: 14 | apt: 15 | update: true 16 | matrix: 17 | include: 18 | - python: 3.5 19 | - python: 3.6 20 | - python: 3.7 21 | install: pip install . 22 | script: 23 | - python setup.py test 24 | notifications: 25 | email: false 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Gitcoin Core 2 | founders@gitcoin.co 3 | 4 | 5 | GNU AFFERO GENERAL PUBLIC LICENSE 6 | Version 3, 19 November 2007 7 | 8 | Copyright (C) 2007 Free Software Foundation, Inc. 9 | Everyone is permitted to copy and distribute verbatim copies 10 | of this license document, but changing it is not allowed. 11 | 12 | Preamble 13 | 14 | The GNU Affero General Public License is a free, copyleft license for 15 | software and other kinds of works, specifically designed to ensure 16 | cooperation with the community in the case of network server software. 17 | 18 | The licenses for most software and other practical works are designed 19 | to take away your freedom to share and change the works. By contrast, 20 | our General Public Licenses are intended to guarantee your freedom to 21 | share and change all versions of a program--to make sure it remains free 22 | software for all its users. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | them if you wish), that you receive source code or can get it if you 28 | want it, that you can change the software or use pieces of it in new 29 | free programs, and that you know you can do these things. 30 | 31 | Developers that use our General Public Licenses protect your rights 32 | with two steps: (1) assert copyright on the software, and (2) offer 33 | you this License which gives you legal permission to copy, distribute 34 | and/or modify the software. 35 | 36 | A secondary benefit of defending all users' freedom is that 37 | improvements made in alternate versions of the program, if they 38 | receive widespread use, become available for other developers to 39 | incorporate. Many developers of free software are heartened and 40 | encouraged by the resulting cooperation. However, in the case of 41 | software used on network servers, this result may fail to come about. 42 | The GNU General Public License permits making a modified version and 43 | letting the public access it on a server without ever releasing its 44 | source code to the public. 45 | 46 | The GNU Affero General Public License is designed specifically to 47 | ensure that, in such cases, the modified source code becomes available 48 | to the community. It requires the operator of a network server to 49 | provide the source code of the modified version running there to the 50 | users of that server. Therefore, public use of a modified version, on 51 | a publicly accessible server, gives the public access to the source 52 | code of the modified version. 53 | 54 | An older license, called the Affero General Public License and 55 | published by Affero, was designed to accomplish similar goals. This is 56 | a different license, not a version of the Affero GPL, but Affero has 57 | released a new version of the Affero GPL which permits relicensing under 58 | this license. 59 | 60 | The precise terms and conditions for copying, distribution and 61 | modification follow. 62 | 63 | TERMS AND CONDITIONS 64 | 65 | 0. Definitions. 66 | 67 | "This License" refers to version 3 of the GNU Affero General Public License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds of 70 | works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of an 78 | exact copy. The resulting work is called a "modified version" of the 79 | earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user through 93 | a computer network, with no transfer of a copy, is not conveying. 94 | 95 | An interactive user interface displays "Appropriate Legal Notices" 96 | to the extent that it includes a convenient and prominently visible 97 | feature that (1) displays an appropriate copyright notice, and (2) 98 | tells the user that there is no warranty for the work (except to the 99 | extent that warranties are provided), that licensees may convey the 100 | work under this License, and how to view a copy of this License. If 101 | the interface presents a list of user commands or options, such as a 102 | menu, a prominent item in the list meets this criterion. 103 | 104 | 1. Source Code. 105 | 106 | The "source code" for a work means the preferred form of the work 107 | for making modifications to it. "Object code" means any non-source 108 | form of a work. 109 | 110 | A "Standard Interface" means an interface that either is an official 111 | standard defined by a recognized standards body, or, in the case of 112 | interfaces specified for a particular programming language, one that 113 | is widely used among developers working in that language. 114 | 115 | The "System Libraries" of an executable work include anything, other 116 | than the work as a whole, that (a) is included in the normal form of 117 | packaging a Major Component, but which is not part of that Major 118 | Component, and (b) serves only to enable use of the work with that 119 | Major Component, or to implement a Standard Interface for which an 120 | implementation is available to the public in source code form. A 121 | "Major Component", in this context, means a major essential component 122 | (kernel, window system, and so on) of the specific operating system 123 | (if any) on which the executable work runs, or a compiler used to 124 | produce the work, or an object code interpreter used to run it. 125 | 126 | The "Corresponding Source" for a work in object code form means all 127 | the source code needed to generate, install, and (for an executable 128 | work) run the object code and to modify the work, including scripts to 129 | control those activities. However, it does not include the work's 130 | System Libraries, or general-purpose tools or generally available free 131 | programs which are used unmodified in performing those activities but 132 | which are not part of the work. For example, Corresponding Source 133 | includes interface definition files associated with source files for 134 | the work, and the source code for shared libraries and dynamically 135 | linked subprograms that the work is specifically designed to require, 136 | such as by intimate data communication or control flow between those 137 | subprograms and other parts of the work. 138 | 139 | The Corresponding Source need not include anything that users 140 | can regenerate automatically from other parts of the Corresponding 141 | Source. 142 | 143 | The Corresponding Source for a work in source code form is that 144 | same work. 145 | 146 | 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not 157 | convey, without conditions so long as your license otherwise remains 158 | in force. You may convey covered works to others for the sole purpose 159 | of having them make modifications exclusively for you, or provide you 160 | with facilities for running those works, provided that you comply with 161 | the terms of this License in conveying all material for which you do 162 | not control copyright. Those thus making or running the covered works 163 | for you must do so exclusively on your behalf, under your direction 164 | and control, on terms that prohibit them from making any copies of 165 | your copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under 168 | the conditions stated below. Sublicensing is not allowed; section 10 169 | makes it unnecessary. 170 | 171 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such circumvention 181 | is effected by exercising rights under this License with respect to 182 | the covered work, and you disclaim any intention to limit operation or 183 | modification of the work as a means of enforcing, against the work's 184 | users, your or third parties' legal rights to forbid circumvention of 185 | technological measures. 186 | 187 | 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these conditions: 205 | 206 | a) The work must carry prominent notices stating that you modified 207 | it, and giving a relevant date. 208 | 209 | b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under section 211 | 7. This requirement modifies the requirement in section 4 to 212 | "keep intact all notices". 213 | 214 | c) You must license the entire work, as a whole, under this 215 | License to anyone who comes into possession of a copy. This 216 | License will therefore apply, along with any applicable section 7 217 | additional terms, to the whole of the work, and all its parts, 218 | regardless of how they are packaged. This License gives no 219 | permission to license the work in any other way, but it does not 220 | invalidate such permission if you have separately received it. 221 | 222 | d) If the work has interactive user interfaces, each must display 223 | Appropriate Legal Notices; however, if the Program has interactive 224 | interfaces that do not display Appropriate Legal Notices, your 225 | work need not make them do so. 226 | 227 | A compilation of a covered work with other separate and independent 228 | works, which are not by their nature extensions of the covered work, 229 | and which are not combined with it such as to form a larger program, 230 | in or on a volume of a storage or distribution medium, is called an 231 | "aggregate" if the compilation and its resulting copyright are not 232 | used to limit the access or legal rights of the compilation's users 233 | beyond what the individual works permit. Inclusion of a covered work 234 | in an aggregate does not cause this License to apply to the other 235 | parts of the aggregate. 236 | 237 | 6. Conveying Non-Source Forms. 238 | 239 | You may convey a covered work in object code form under the terms 240 | of sections 4 and 5, provided that you also convey the 241 | machine-readable Corresponding Source under the terms of this License, 242 | in one of these ways: 243 | 244 | a) Convey the object code in, or embodied in, a physical product 245 | (including a physical distribution medium), accompanied by the 246 | Corresponding Source fixed on a durable physical medium 247 | customarily used for software interchange. 248 | 249 | b) Convey the object code in, or embodied in, a physical product 250 | (including a physical distribution medium), accompanied by a 251 | written offer, valid for at least three years and valid for as 252 | long as you offer spare parts or customer support for that product 253 | model, to give anyone who possesses the object code either (1) a 254 | copy of the Corresponding Source for all the software in the 255 | product that is covered by this License, on a durable physical 256 | medium customarily used for software interchange, for a price no 257 | more than your reasonable cost of physically performing this 258 | conveying of source, or (2) access to copy the 259 | Corresponding Source from a network server at no charge. 260 | 261 | c) Convey individual copies of the object code with a copy of the 262 | written offer to provide the Corresponding Source. This 263 | alternative is allowed only occasionally and noncommercially, and 264 | only if you received the object code with such an offer, in accord 265 | with subsection 6b. 266 | 267 | d) Convey the object code by offering access from a designated 268 | place (gratis or for a charge), and offer equivalent access to the 269 | Corresponding Source in the same way through the same place at no 270 | further charge. You need not require recipients to copy the 271 | Corresponding Source along with the object code. If the place to 272 | copy the object code is a network server, the Corresponding Source 273 | may be on a different server (operated by you or a third party) 274 | that supports equivalent copying facilities, provided you maintain 275 | clear directions next to the object code saying where to find the 276 | Corresponding Source. Regardless of what server hosts the 277 | Corresponding Source, you remain obligated to ensure that it is 278 | available for as long as needed to satisfy these requirements. 279 | 280 | e) Convey the object code using peer-to-peer transmission, provided 281 | you inform other peers where the object code and Corresponding 282 | Source of the work are being offered to the general public at no 283 | charge under subsection 6d. 284 | 285 | A separable portion of the object code, whose source code is excluded 286 | from the Corresponding Source as a System Library, need not be 287 | included in conveying the object code work. 288 | 289 | A "User Product" is either (1) a "consumer product", which means any 290 | tangible personal property which is normally used for personal, family, 291 | or household purposes, or (2) anything designed or sold for incorporation 292 | into a dwelling. In determining whether a product is a consumer product, 293 | doubtful cases shall be resolved in favor of coverage. For a particular 294 | product received by a particular user, "normally used" refers to a 295 | typical or common use of that class of product, regardless of the status 296 | of the particular user or of the way in which the particular user 297 | actually uses, or expects or is expected to use, the product. A product 298 | is a consumer product regardless of whether the product has substantial 299 | commercial, industrial or non-consumer uses, unless such uses represent 300 | the only significant mode of use of the product. 301 | 302 | "Installation Information" for a User Product means any methods, 303 | procedures, authorization keys, or other information required to install 304 | and execute modified versions of a covered work in that User Product from 305 | a modified version of its Corresponding Source. The information must 306 | suffice to ensure that the continued functioning of the modified object 307 | code is in no case prevented or interfered with solely because 308 | modification has been made. 309 | 310 | If you convey an object code work under this section in, or with, or 311 | specifically for use in, a User Product, and the conveying occurs as 312 | part of a transaction in which the right of possession and use of the 313 | User Product is transferred to the recipient in perpetuity or for a 314 | fixed term (regardless of how the transaction is characterized), the 315 | Corresponding Source conveyed under this section must be accompanied 316 | by the Installation Information. But this requirement does not apply 317 | if neither you nor any third party retains the ability to install 318 | modified object code on the User Product (for example, the work has 319 | been installed in ROM). 320 | 321 | The requirement to provide Installation Information does not include a 322 | requirement to continue to provide support service, warranty, or updates 323 | for a work that has been modified or installed by the recipient, or for 324 | the User Product in which it has been modified or installed. Access to a 325 | network may be denied when the modification itself materially and 326 | adversely affects the operation of the network or violates the rules and 327 | protocols for communication across the network. 328 | 329 | Corresponding Source conveyed, and Installation Information provided, 330 | in accord with this section must be in a format that is publicly 331 | documented (and with an implementation available to the public in 332 | source code form), and must require no special password or key for 333 | unpacking, reading or copying. 334 | 335 | 7. Additional Terms. 336 | 337 | "Additional permissions" are terms that supplement the terms of this 338 | License by making exceptions from one or more of its conditions. 339 | Additional permissions that are applicable to the entire Program shall 340 | be treated as though they were included in this License, to the extent 341 | that they are valid under applicable law. If additional permissions 342 | apply only to part of the Program, that part may be used separately 343 | under those permissions, but the entire Program remains governed by 344 | this License without regard to the additional permissions. 345 | 346 | When you convey a copy of a covered work, you may at your option 347 | remove any additional permissions from that copy, or from any part of 348 | it. (Additional permissions may be written to require their own 349 | removal in certain cases when you modify the work.) You may place 350 | additional permissions on material, added by you to a covered work, 351 | for which you have or can give appropriate copyright permission. 352 | 353 | Notwithstanding any other provision of this License, for material you 354 | add to a covered work, you may (if authorized by the copyright holders of 355 | that material) supplement the terms of this License with terms: 356 | 357 | a) Disclaiming warranty or limiting liability differently from the 358 | terms of sections 15 and 16 of this License; or 359 | 360 | b) Requiring preservation of specified reasonable legal notices or 361 | author attributions in that material or in the Appropriate Legal 362 | Notices displayed by works containing it; or 363 | 364 | c) Prohibiting misrepresentation of the origin of that material, or 365 | requiring that modified versions of such material be marked in 366 | reasonable ways as different from the original version; or 367 | 368 | d) Limiting the use for publicity purposes of names of licensors or 369 | authors of the material; or 370 | 371 | e) Declining to grant rights under trademark law for use of some 372 | trade names, trademarks, or service marks; or 373 | 374 | f) Requiring indemnification of licensors and authors of that 375 | material by anyone who conveys the material (or modified versions of 376 | it) with contractual assumptions of liability to the recipient, for 377 | any liability that these contractual assumptions directly impose on 378 | those licensors and authors. 379 | 380 | All other non-permissive additional terms are considered "further 381 | restrictions" within the meaning of section 10. If the Program as you 382 | received it, or any part of it, contains a notice stating that it is 383 | governed by this License along with a term that is a further 384 | restriction, you may remove that term. If a license document contains 385 | a further restriction but permits relicensing or conveying under this 386 | License, you may add to a covered work material governed by the terms 387 | of that license document, provided that the further restriction does 388 | not survive such relicensing or conveying. 389 | 390 | If you add terms to a covered work in accord with this section, you 391 | must place, in the relevant source files, a statement of the 392 | additional terms that apply to those files, or a notice indicating 393 | where to find the applicable terms. 394 | 395 | Additional terms, permissive or non-permissive, may be stated in the 396 | form of a separately written license, or stated as exceptions; 397 | the above requirements apply either way. 398 | 399 | 8. Termination. 400 | 401 | You may not propagate or modify a covered work except as expressly 402 | provided under this License. Any attempt otherwise to propagate or 403 | modify it is void, and will automatically terminate your rights under 404 | this License (including any patent licenses granted under the third 405 | paragraph of section 11). 406 | 407 | However, if you cease all violation of this License, then your 408 | license from a particular copyright holder is reinstated (a) 409 | provisionally, unless and until the copyright holder explicitly and 410 | finally terminates your license, and (b) permanently, if the copyright 411 | holder fails to notify you of the violation by some reasonable means 412 | prior to 60 days after the cessation. 413 | 414 | Moreover, your license from a particular copyright holder is 415 | reinstated permanently if the copyright holder notifies you of the 416 | violation by some reasonable means, this is the first time you have 417 | received notice of violation of this License (for any work) from that 418 | copyright holder, and you cure the violation prior to 30 days after 419 | your receipt of the notice. 420 | 421 | Termination of your rights under this section does not terminate the 422 | licenses of parties who have received copies or rights from you under 423 | this License. If your rights have been terminated and not permanently 424 | reinstated, you do not qualify to receive new licenses for the same 425 | material under section 10. 426 | 427 | 9. Acceptance Not Required for Having Copies. 428 | 429 | You are not required to accept this License in order to receive or 430 | run a copy of the Program. Ancillary propagation of a covered work 431 | occurring solely as a consequence of using peer-to-peer transmission 432 | to receive a copy likewise does not require acceptance. However, 433 | nothing other than this License grants you permission to propagate or 434 | modify any covered work. These actions infringe copyright if you do 435 | not accept this License. Therefore, by modifying or propagating a 436 | covered work, you indicate your acceptance of this License to do so. 437 | 438 | 10. Automatic Licensing of Downstream Recipients. 439 | 440 | Each time you convey a covered work, the recipient automatically 441 | receives a license from the original licensors, to run, modify and 442 | propagate that work, subject to this License. You are not responsible 443 | for enforcing compliance by third parties with this License. 444 | 445 | An "entity transaction" is a transaction transferring control of an 446 | organization, or substantially all assets of one, or subdividing an 447 | organization, or merging organizations. If propagation of a covered 448 | work results from an entity transaction, each party to that 449 | transaction who receives a copy of the work also receives whatever 450 | licenses to the work the party's predecessor in interest had or could 451 | give under the previous paragraph, plus a right to possession of the 452 | Corresponding Source of the work from the predecessor in interest, if 453 | the predecessor has it or can get it with reasonable efforts. 454 | 455 | You may not impose any further restrictions on the exercise of the 456 | rights granted or affirmed under this License. For example, you may 457 | not impose a license fee, royalty, or other charge for exercise of 458 | rights granted under this License, and you may not initiate litigation 459 | (including a cross-claim or counterclaim in a lawsuit) alleging that 460 | any patent claim is infringed by making, using, selling, offering for 461 | sale, or importing the Program or any portion of it. 462 | 463 | 11. Patents. 464 | 465 | A "contributor" is a copyright holder who authorizes use under this 466 | License of the Program or a work on which the Program is based. The 467 | work thus licensed is called the contributor's "contributor version". 468 | 469 | A contributor's "essential patent claims" are all patent claims 470 | owned or controlled by the contributor, whether already acquired or 471 | hereafter acquired, that would be infringed by some manner, permitted 472 | by this License, of making, using, or selling its contributor version, 473 | but do not include claims that would be infringed only as a 474 | consequence of further modification of the contributor version. For 475 | purposes of this definition, "control" includes the right to grant 476 | patent sublicenses in a manner consistent with the requirements of 477 | this License. 478 | 479 | Each contributor grants you a non-exclusive, worldwide, royalty-free 480 | patent license under the contributor's essential patent claims, to 481 | make, use, sell, offer for sale, import and otherwise run, modify and 482 | propagate the contents of its contributor version. 483 | 484 | In the following three paragraphs, a "patent license" is any express 485 | agreement or commitment, however denominated, not to enforce a patent 486 | (such as an express permission to practice a patent or covenant not to 487 | sue for patent infringement). To "grant" such a patent license to a 488 | party means to make such an agreement or commitment not to enforce a 489 | patent against the party. 490 | 491 | If you convey a covered work, knowingly relying on a patent license, 492 | and the Corresponding Source of the work is not available for anyone 493 | to copy, free of charge and under the terms of this License, through a 494 | publicly available network server or other readily accessible means, 495 | then you must either (1) cause the Corresponding Source to be so 496 | available, or (2) arrange to deprive yourself of the benefit of the 497 | patent license for this particular work, or (3) arrange, in a manner 498 | consistent with the requirements of this License, to extend the patent 499 | license to downstream recipients. "Knowingly relying" means you have 500 | actual knowledge that, but for the patent license, your conveying the 501 | covered work in a country, or your recipient's use of the covered work 502 | in a country, would infringe one or more identifiable patents in that 503 | country that you have reason to believe are valid. 504 | 505 | If, pursuant to or in connection with a single transaction or 506 | arrangement, you convey, or propagate by procuring conveyance of, a 507 | covered work, and grant a patent license to some of the parties 508 | receiving the covered work authorizing them to use, propagate, modify 509 | or convey a specific copy of the covered work, then the patent license 510 | you grant is automatically extended to all recipients of the covered 511 | work and works based on it. 512 | 513 | A patent license is "discriminatory" if it does not include within 514 | the scope of its coverage, prohibits the exercise of, or is 515 | conditioned on the non-exercise of one or more of the rights that are 516 | specifically granted under this License. You may not convey a covered 517 | work if you are a party to an arrangement with a third party that is 518 | in the business of distributing software, under which you make payment 519 | to the third party based on the extent of your activity of conveying 520 | the work, and under which the third party grants, to any of the 521 | parties who would receive the covered work from you, a discriminatory 522 | patent license (a) in connection with copies of the covered work 523 | conveyed by you (or copies made from those copies), or (b) primarily 524 | for and in connection with specific products or compilations that 525 | contain the covered work, unless you entered into that arrangement, 526 | or that patent license was granted, prior to 28 March 2007. 527 | 528 | Nothing in this License shall be construed as excluding or limiting 529 | any implied license or other defenses to infringement that may 530 | otherwise be available to you under applicable patent law. 531 | 532 | 12. No Surrender of Others' Freedom. 533 | 534 | If conditions are imposed on you (whether by court order, agreement or 535 | otherwise) that contradict the conditions of this License, they do not 536 | excuse you from the conditions of this License. If you cannot convey a 537 | covered work so as to satisfy simultaneously your obligations under this 538 | License and any other pertinent obligations, then as a consequence you may 539 | not convey it at all. For example, if you agree to terms that obligate you 540 | to collect a royalty for further conveying from those to whom you convey 541 | the Program, the only way you could satisfy both those terms and this 542 | License would be to refrain entirely from conveying the Program. 543 | 544 | 13. Remote Network Interaction; Use with the GNU General Public License. 545 | 546 | Notwithstanding any other provision of this License, if you modify the 547 | Program, your modified version must prominently offer all users 548 | interacting with it remotely through a computer network (if your version 549 | supports such interaction) an opportunity to receive the Corresponding 550 | Source of your version by providing access to the Corresponding Source 551 | from a network server at no charge, through some standard or customary 552 | means of facilitating copying of software. This Corresponding Source 553 | shall include the Corresponding Source for any work covered by version 3 554 | of the GNU General Public License that is incorporated pursuant to the 555 | following paragraph. 556 | 557 | Notwithstanding any other provision of this License, you have 558 | permission to link or combine any covered work with a work licensed 559 | under version 3 of the GNU General Public License into a single 560 | combined work, and to convey the resulting work. The terms of this 561 | License will continue to apply to the part which is the covered work, 562 | but the work with which it is combined will remain governed by version 563 | 3 of the GNU General Public License. 564 | 565 | 14. Revised Versions of this License. 566 | 567 | The Free Software Foundation may publish revised and/or new versions of 568 | the GNU Affero General Public License from time to time. Such new versions 569 | will be similar in spirit to the present version, but may differ in detail to 570 | address new problems or concerns. 571 | 572 | Each version is given a distinguishing version number. If the 573 | Program specifies that a certain numbered version of the GNU Affero General 574 | Public License "or any later version" applies to it, you have the 575 | option of following the terms and conditions either of that numbered 576 | version or of any later version published by the Free Software 577 | Foundation. If the Program does not specify a version number of the 578 | GNU Affero General Public License, you may choose any version ever published 579 | by the Free Software Foundation. 580 | 581 | If the Program specifies that a proxy can decide which future 582 | versions of the GNU Affero General Public License can be used, that proxy's 583 | public statement of acceptance of a version permanently authorizes you 584 | to choose that version for the Program. 585 | 586 | Later license versions may give you additional or different 587 | permissions. However, no additional obligations are imposed on any 588 | author or copyright holder as a result of your choosing to follow a 589 | later version. 590 | 591 | 15. Disclaimer of Warranty. 592 | 593 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 594 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 595 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 596 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 597 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 598 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 599 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 600 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 601 | 602 | 16. Limitation of Liability. 603 | 604 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 605 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 606 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 607 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 608 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 609 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 610 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 611 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 612 | SUCH DAMAGES. 613 | 614 | 17. Interpretation of Sections 15 and 16. 615 | 616 | If the disclaimer of warranty and limitation of liability provided 617 | above cannot be given local legal effect according to their terms, 618 | reviewing courts shall apply local law that most closely approximates 619 | an absolute waiver of all civil liability in connection with the 620 | Program, unless a warranty or assumption of liability accompanies a 621 | copy of the Program in return for a fee. 622 | 623 | END OF TERMS AND CONDITIONS 624 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Help 2 | .DEFAULT_GOAL := help 3 | 4 | .PHONY: help 5 | 6 | build: ## Build the pypi package and wheels. 7 | @python setup.py sdist bdist_wheel 8 | 9 | deploy: install ## Deploy package and wheel to PyPi. 10 | @twine upload dist/* 11 | 12 | deploy-test: install ## Deploy package and wheel to PyPi test environment. 13 | @twine upload --repository-url https://test.pypi.org/legacy/ dist/* 14 | 15 | install: ## Install local, editable version of the gitcoin client. 16 | @pip install -e . 17 | 18 | test: ## Run pytest. 19 | @python setup.py test 20 | 21 | fix-isort: ## Run isort against python files in the project directory. 22 | @isort -rc --atomic ./gitcoin ./tests 23 | 24 | fix-yapf: ## Run yapf against any included or newly introduced Python code. 25 | @yapf -i -r -p ./gitcoin ./tests 26 | 27 | fix: fix-isort fix-yapf ## Attempt to run all fixes against the project directory. 28 | 29 | help: 30 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitcoin Python API Client 2 | 3 | [![Build Status](https://travis-ci.com/gitcoinco/python-api-client.svg?branch=master)](https://travis-ci.com/gitcoinco/python-api-client) 4 | 5 | This Python package provides the `bounties` endpoint of the Gitcoin API, which allows you to: 6 | 7 | - list all bounties 8 | - list all bounties which meet certain conditions (i.e. filter them) 9 | - retrieve a single bounty by it's primary key 10 | 11 | ## Install via pypi 12 | 13 | ```bash 14 | pip install gitcoin 15 | ``` 16 | 17 | ## Usage Examples 18 | 19 | ### List all bounties 20 | 21 | ```python 22 | from gitcoin import Gitcoin 23 | api = Gitcoin() 24 | all_bounties = api.bounties.all() 25 | ``` 26 | 27 | ### List all open bounties 28 | 29 | ```python 30 | from gitcoin import Gitcoin 31 | api = Gitcoin() 32 | open_bounties = api.bounties.filter(is_open=True).all() 33 | ``` 34 | 35 | ### List all open "Beginner" bounties 36 | 37 | ```python 38 | from gitcoin import Gitcoin 39 | api = Gitcoin() 40 | bounties_api = api.bounties 41 | bounties_api.filter(is_open=True) 42 | bounties_api.filter(experience_level='Beginner') 43 | open_beginner_bounties = bounties_api.all() 44 | ``` 45 | 46 | The example above has been reformatted for easier reading. A [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Python) is also available. Please scroll the following code block all the way to the end to see the whole line: 47 | 48 | ```python 49 | from gitcoin import Gitcoin 50 | api = Gitcoin() 51 | open_beginner_bounties = api.bounties.filter(is_open=True, experience_level='Beginner').all() 52 | ``` 53 | 54 | ### List all open bounties marked for either "Beginner" OR "Intermediate" experience level 55 | 56 | For some filter conditions, multiple different values can be given, which results in a logical `OR` for that condition: 57 | 58 | ```python 59 | from gitcoin import Gitcoin 60 | api = Gitcoin() 61 | bounties_api = api.bounties 62 | bounties_api.filter(is_open=True) 63 | bounties_api.filter(experience_level='Beginner') 64 | bounties_api.filter(experience_level='Intermediate') 65 | open_beginner_and_intermediate_bounties = bounties_api.all() 66 | ``` 67 | 68 | The example above has been reformatted for easier reading. A [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Python) is also available. Please scroll the following code block all the way to the end to see the whole line: 69 | 70 | ```python 71 | from gitcoin import Gitcoin 72 | api = Gitcoin() 73 | open_beginner_and_intermediate_bounties = api.bounties.filter(is_open=True).filter(experience_level='Beginner').filter(experience_level='Intermediate').all() 74 | ``` 75 | 76 | ## API 77 | 78 | ### Instantiation 79 | 80 | 1. Create a `Gitcoin()` API root object: 81 | ```python 82 | from gitcoin import Gitcoin 83 | api = Gitcoin() 84 | ``` 85 | 2. The `bounties` API endpoint is accessible as a property of the API root object: 86 | ```python 87 | bounties_endpoint = api.bounties 88 | ``` 89 | Each access to the `bounties` property results in a new `Endpoint` object with no filter conditions or any other parameters (like sorting) set. If you want to keep a specific set of filter conditions, simply store the `Endpoint` object in a variable instead of referring to the `bounties` property of the root object. 90 | 91 | ### `bounties.filter(**kwargs)` 92 | 93 | Limit the list of bounties returned by either `get_page()` or `all()` to those bounties meeting the filter condition(s). For some filter conditions, multiple different values can be given, which results in a logical `OR` for that condition. 94 | 95 | The condition name is the name of the keyword argument, and the condition value is the value of the keyword argument: 96 | 97 | ```python 98 | open_bounties = api.bounties.filter(is_open=True).all() 99 | ``` 100 | 101 | Conditions with different names can be given in one `filter()` call: 102 | 103 | ```python 104 | open_beginner bounties = api.bounties.filter(is_open=True, experience_level='Beginner').all() 105 | ``` 106 | 107 | Or if preferred, they can also be given in separate `filter()` calls: 108 | 109 | ```python 110 | open_beginner bounties = api.bounties.filter(is_open=True).filter(experience_level='Beginner').all() 111 | ``` 112 | 113 | Giving multiple values for the same filter condition has to be done in separate calls to `filter()`: 114 | 115 | ```python 116 | beginner_and_intermediate_bounties = api.bounties.filter(experience_level='Beginner').filter(experience_level='Intermediate').all() 117 | ``` 118 | 119 | For the following filter conditions, multiple values can be given to achieve a logical `OR`: 120 | 121 | - `experience_level (str)` 122 | - `project_length (str)` 123 | - `bounty_type (str)` 124 | - `bounty_owner_address (str)` 125 | - `bounty_owner_github_username (str)` 126 | - `idx_status (str)` 127 | - `network (str)` 128 | - `standard_bounties_id (int)` 129 | - `github_url (str)` 130 | - `raw_data (str)` 131 | 132 | The following filter conditions are **single value**, in other words, multiple values result in the last value overwriting all earlier values: 133 | 134 | - `pk__gt (int)` 135 | - `started (str)` 136 | - `is_open (bool)` 137 | - `fulfiller_github_username (str)` 138 | - `interested_github_username (str)` 139 | 140 | `filter()` returns the `Endpoint` object itself to enable a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Python). 141 | 142 | ### `bounties.order_by(sort)` 143 | 144 | Determine the order of the bounties returned by either `get_page()` or `all()`. The `sort` argument is a `string` containing a DB field name to sort by. It can also have an optional "-" prefix for reversing the direction. Choose from these field names: 145 | 146 | - `accepted` 147 | - `balance` 148 | - `bounty_owner_address` 149 | - `bounty_owner_email` 150 | - `bounty_owner_github_username` 151 | - `bounty_owner_name` 152 | - `bounty_type` 153 | - `canceled_on` 154 | - `contract_address` 155 | - `current_bounty` 156 | - `experience_level` 157 | - `expires_date` 158 | - `fulfillment_accepted_on` 159 | - `fulfillment_started_on` 160 | - `fulfillment_submitted_on` 161 | - `github_comments` 162 | - `github_url` 163 | - `idx_experience_level` 164 | - `idx_project_length` 165 | - `idx_status` 166 | - `interested` 167 | - `interested_comment` 168 | - `is_open` 169 | - `issue_description` 170 | - `last_comment_date` 171 | - `metadata` 172 | - `network` 173 | - `num_fulfillments` 174 | - `override_status` 175 | - `privacy_preferences` 176 | - `project_length` 177 | - `raw_data` 178 | - `snooze_warnings_for_days` 179 | - `standard_bounties_id` 180 | - `submissions_comment` 181 | - `title` 182 | - `token_address` 183 | - `token_name` 184 | - `token_value_in_usdt` 185 | - `token_value_time_peg` 186 | - `_val_usd_db` 187 | - `value_in_eth` 188 | - `value_in_token` 189 | - `value_in_usdt` 190 | - `value_in_usdt_now` 191 | - `value_true` 192 | - `web3_created` 193 | - `web3_type` 194 | 195 | `order_by()` returns the `Endpoint` object itself to enable a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Python). 196 | 197 | ### `bounties.get_page(number=1, per_page=25)` 198 | 199 | Returns one page of the (potentially `filter()`ed) `list` of bounties with the given 1-based index `number (int)`. The page size can be set with `per_page (int)`. Each bounty is a `dict`, basically the direct output of [`requests`' `.json()`](http://docs.python-requests.org/en/master/user/quickstart/#json-response-content) call. 200 | 201 | ### `bounties.all()` 202 | 203 | Returns the complete (potentially `filter()`ed) `list` of bounties. Each bounty is a `dict`, basically the direct output of [`requests`' `.json()`](http://docs.python-requests.org/en/master/user/quickstart/#json-response-content) call. 204 | 205 | ### `bounties.get(primary_key)` 206 | 207 | Returns one (1) bounty as specified by `primary_key (int)`. It is returned as a `dict`, basically the direct output of [`requests`' `.json()`](http://docs.python-requests.org/en/master/user/quickstart/#json-response-content) call. 208 | 209 | ------------------------- 210 | 211 | ## Todo 212 | 213 | - [x] Add base gitcoin.Gitcoin client 214 | - [x] Add `bounties` api filter 215 | - [x] Implement all filter fields present in `gitcoinco/web/app/dashboard/router.py` 216 | - [ ] Add `universe` api filter 217 | - [ ] Implement all filter fields present in `gitcoinco/web/app/external_bounties/router.py` 218 | - [x] Add sorting/order_by 219 | - [x] Add pagination (page/limit) 220 | - [ ] Add travis-ci.com project and twine/pypi credentials. 221 | - [ ] Add codecov.io project. 222 | - [ ] Cut first release (Tag github release, push changes, and let CI deploy to pypi) 223 | - [ ] Maintain +90% coverage 224 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # see https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option 5 | def pytest_addoption(parser): 6 | parser.addoption('--liveapi', action='store_true', default=False, help='run some tests againts live API') 7 | -------------------------------------------------------------------------------- /gitcoin/__init__.py: -------------------------------------------------------------------------------- 1 | """Define the Gitcoin API client.""" 2 | 3 | from gitcoin.client import Config 4 | from gitcoin.client import BountyConfig 5 | from gitcoin.client import Endpoint 6 | from gitcoin.client import Gitcoin 7 | 8 | __all__ = [ 9 | 'Config', 10 | 'BountyConfig', 11 | 'Endpoint', 12 | 'Gitcoin', 13 | ] 14 | -------------------------------------------------------------------------------- /gitcoin/client.py: -------------------------------------------------------------------------------- 1 | """Define the Gitcoin API client.""" 2 | 3 | import gitcoin.validation 4 | import requests 5 | 6 | 7 | class Config: 8 | """Define Base Class for API Endpoint Config.""" 9 | 10 | def __init__(self): 11 | """Init empty params container.""" 12 | self.params = {} 13 | 14 | def has(self, name): 15 | """Tell if a setting for 'name' was defined.""" 16 | return name in self.params 17 | 18 | def get(self, name): 19 | """Get the setting for 'name'.""" 20 | if self.has(name): 21 | return self.params[name] 22 | else: 23 | msg = 'Unknown config "{name}"' 24 | raise KeyError(msg.format(name=name)) 25 | 26 | 27 | class BountyConfig(Config): 28 | """Define 'bounties' API Endpoint Config.""" 29 | 30 | def __init__(self): 31 | """Init params container for 'bounties' filters etc.""" 32 | super().__init__() 33 | self.params = { 34 | 'experience_level': (True, gitcoin.validation.experience_level), 35 | 'project_length': (True, gitcoin.validation.project_length), 36 | 'bounty_type': (True, gitcoin.validation.bounty_type), 37 | 'bounty_owner_address': (True, str), 38 | 'bounty_owner_github_username': (True, str), 39 | 'idx_status': (True, gitcoin.validation.idx_status), 40 | 'network': (True, str), 41 | 'standard_bounties_id': (True, int), 42 | 'pk__gt': (False, int), 43 | 'started': (False, str), 44 | 'is_open': (False, bool), 45 | 'github_url': (True, str), 46 | 'fulfiller_github_username': (False, str), 47 | 'interested_github_username': (False, str), 48 | 'raw_data': (True, str), 49 | 'order_by': (False, gitcoin.validation.order_by), 50 | 'limit': (False, int), 51 | 'offset': (False, int) 52 | } 53 | 54 | 55 | class Endpoint: 56 | """Wrap one Gitcoin API end point.""" 57 | 58 | def __init__(self, url, config): 59 | """Inject URL and Config, default to no query parameters.""" 60 | self.url = url 61 | self.config = config 62 | self.params = {} 63 | 64 | def _add_param(self, name, value): 65 | """Add query parameter with safeguards.""" 66 | if self.config.has(name): 67 | is_multiple, normalize = self.config.get(name) 68 | if not is_multiple: 69 | self._del_param(name) # Throw away all previous values, if any. 70 | if callable(normalize): 71 | value = normalize(value) 72 | self._add_param_unchecked(name, value) 73 | return self 74 | else: 75 | msg = 'Tried to filter by unknown param "{name}".' 76 | raise KeyError(msg.format(name=name)) 77 | 78 | def _del_param(self, name): 79 | """Delete query parameter.""" 80 | if name in self.params: 81 | del self.params[name] 82 | return self 83 | 84 | def _reset_all_params(self): 85 | """Delete all query parameters.""" 86 | self.params = {} 87 | 88 | def _add_param_unchecked(self, name, value): 89 | """Add query parameter without safeguards. 90 | 91 | This is available in case this API client is out-of-sync with the API. 92 | """ 93 | if name not in self.params: 94 | self.params[name] = [] 95 | self.params[name].append(str(value)) 96 | return self 97 | 98 | def filter(self, **kwargs): 99 | """Filter the result set.""" 100 | for name, value in kwargs.items(): 101 | self._add_param(name, value) 102 | return self 103 | 104 | def order_by(self, sort): 105 | """Sort the result set.""" 106 | self._add_param('order_by', sort) 107 | return self 108 | 109 | def get_page(self, number=1, per_page=25): 110 | """Get one page of the resources list.""" 111 | self._add_param('limit', per_page) 112 | self._add_param('offset', (number - 1) * per_page) 113 | return self._request_get() 114 | 115 | def all(self): 116 | """List all resources.""" 117 | self._del_param('limit') 118 | self._del_param('offset') 119 | return self._request_get() 120 | 121 | def get(self, primary_key): 122 | """Retrieve one resource by primary key.""" 123 | return self._request_get(''.join((self.url, str(primary_key)))) 124 | 125 | def _request_get(self, url=None): 126 | """Fire the actual HTTP GET request as configured.""" 127 | url = url if url else self.url 128 | params = self._prep_get_params() 129 | response = requests.get(url, params=params) 130 | response.raise_for_status() # Let API consumer know about HTTP errors. 131 | return response.json() 132 | 133 | def _prep_get_params(self): 134 | """Send multi-value fields separated by comma.""" 135 | return {name: ','.join(value) for name, value in self.params.items()} 136 | 137 | 138 | class Gitcoin: 139 | """Provide main API entry point.""" 140 | 141 | def __init__(self): 142 | """Set defaults.""" 143 | self.classes = {} 144 | self.set_class('endpoint', Endpoint) 145 | self.set_class('bounties_list_config', BountyConfig) 146 | self.urls = {} 147 | self.set_url('bounties', 'https://gitcoin.co/api/v0.1/bounties/') 148 | 149 | def set_class(self, cls_id, cls): 150 | """Inject class dependency, overriding the default class.""" 151 | self.classes[cls_id] = cls 152 | 153 | def set_url(self, cls_id, url): 154 | """Configure API URL, overriding the default URL.""" 155 | self.urls[cls_id] = url 156 | 157 | @property 158 | def bounties(self): 159 | """Wrap the 'bounties' API endpoint.""" 160 | url = self.urls['bounties'] 161 | endpoint_class = self.classes['endpoint'] 162 | config_class = self.classes['bounties_list_config'] 163 | return endpoint_class(url, config_class()) 164 | -------------------------------------------------------------------------------- /gitcoin/validation.py: -------------------------------------------------------------------------------- 1 | """Validate parameter values for the Gitcoin API client.""" 2 | 3 | # Valid parameter values as seen at 4 | # https://github.com/gitcoinco/web/blob/84babc30611c281c817582b4d677dda6366def83/app/dashboard/models.py#L119-L168 5 | OPTIONS = { 6 | 'experience_level': ['Beginner', 'Intermediate', 'Advanced', 'Unknown'], 7 | 'project_length': ['Hours', 'Days', 'Weeks', 'Months', 'Unknown'], 8 | 'bounty_type': ['Bug', 'Security', 'Feature', 'Unknown'], 9 | 'idx_status': ['cancelled', 'done', 'expired', 'open', 'started', 'submitted', 'unknown'], 10 | 'order_by': [ 11 | 'web3_type', 'title', 'web3_created', 'value_in_token', 'token_name', 'token_address', 'bounty_type', 12 | 'project_length', 'experience_level', 'github_url', 'github_comments', 'bounty_owner_address', 13 | 'bounty_owner_email', 'bounty_owner_github_username', 'bounty_owner_name', 'is_open', 'expires_date', 14 | 'raw_data', 'metadata', 'current_bounty', '_val_usd_db', 'contract_address', 'network', 'idx_experience_level', 15 | 'idx_project_length', 'idx_status', 'issue_description', 'standard_bounties_id', 'num_fulfillments', 'balance', 16 | 'accepted', 'interested', 'interested_comment', 'submissions_comment', 'override_status', 'last_comment_date', 17 | 'fulfillment_accepted_on', 'fulfillment_submitted_on', 'fulfillment_started_on', 'canceled_on', 18 | 'snooze_warnings_for_days', 'token_value_time_peg', 'token_value_in_usdt', 'value_in_usdt_now', 'value_in_usdt', 19 | 'value_in_eth', 'value_true', 'privacy_preferences' 20 | ] 21 | } 22 | 23 | 24 | def _validate_options(field_name, value): 25 | """Validate values for the given field name.""" 26 | if value in OPTIONS[field_name]: 27 | return value 28 | msg = 'Unknown value "{val}" for field "{name}".' 29 | raise ValueError(msg.format(val=value, name=field_name)) 30 | 31 | 32 | def experience_level(value): 33 | """Validate values for "experience_level".""" 34 | return _validate_options('experience_level', value) 35 | 36 | 37 | def project_length(value): 38 | """Validate values for "project_length".""" 39 | return _validate_options('project_length', value) 40 | 41 | 42 | def bounty_type(value): 43 | """Validate values for "bounty_type".""" 44 | return _validate_options('bounty_type', value) 45 | 46 | 47 | def idx_status(value): 48 | """Validate values for "idx_status".""" 49 | return _validate_options('idx_status', value) 50 | 51 | 52 | def order_by(direction): 53 | """Validate values for "order_by".""" 54 | if direction in OPTIONS['order_by']: 55 | return direction 56 | if direction[0:1] == '-' and direction[1:] in OPTIONS['order_by']: 57 | return direction 58 | msg = 'Unknown direction "{dir}" to order by.' 59 | raise ValueError(msg.format(dir=direction)) 60 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apipkg==1.4 2 | attrs==18.1.0 3 | certifi==2018.4.16 4 | chardet==3.0.4 5 | cookies==2.2.1 6 | coverage==4.5.1 7 | execnet==1.5.0 8 | flake8==3.5.0 9 | idna==2.6 10 | isort==4.3.4 11 | mccabe==0.6.1 12 | more-itertools==4.1.0 13 | pluggy==0.6.0 14 | py==1.5.3 15 | pycodestyle==2.3.1 16 | pyflakes==1.6.0 17 | pytest==3.5.1 18 | pytest-cache==1.0 19 | pytest-cov==2.5.1 20 | pytest-isort==0.2.0 21 | pytest-runner==4.2 22 | requests==2.20.1 23 | responses==0.9.0 24 | six==1.11.0 25 | urllib3==1.24.1 26 | yapf==0.22.0 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE.txt 3 | 4 | [bdist_wheel] 5 | universal=0 6 | 7 | [tool:pytest] 8 | norecursedirs = 9 | .git 10 | .env 11 | venv 12 | migrations 13 | .eggs 14 | python_files = 15 | test_*.py 16 | *_test.py 17 | tests.py 18 | addopts = 19 | -rf 20 | --isort 21 | 22 | [flake8] 23 | max-line-length = 120 24 | exclude = .tox,.git,docs,.eggs 25 | 26 | [pycodestyle] 27 | max-line-length = 120 28 | exclude=.tox,.git,docs,.eggs 29 | 30 | [coverage:run] 31 | branch = True 32 | source = 33 | app 34 | omit = 35 | *.eggs* 36 | *tests* 37 | */__init__.py 38 | 39 | [coverage:report] 40 | # Regexes for lines to exclude from consideration 41 | exclude_lines = 42 | # Have to re-enable the standard pragma 43 | pragma: no cover 44 | 45 | # Don't complain about missing debug-only code: 46 | def __repr__ 47 | if self\.debug 48 | 49 | # Don't complain if tests don't hit defensive assertion code: 50 | raise AssertionError 51 | raise NotImplementedError 52 | 53 | # Don't complain if non-runnable code isn't run: 54 | if 0: 55 | if __name__ == .__main__.: 56 | 57 | ignore_errors = True 58 | 59 | [coverage:html] 60 | directory = coverage_html_report 61 | 62 | [isort] 63 | line_length=120 64 | multi_line_output=5 65 | include_trailing_comma=True 66 | known_future_library=future,pies 67 | known_third_party=pytest,requests 68 | default_section=THIRDPARTY 69 | indent=' ' 70 | sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 71 | 72 | [yapf] 73 | based_on_style = pep8 74 | column_limit = 120 75 | indent_width = 4 76 | spaces_before_comment = 2 77 | ALLOW_SPLIT_BEFORE_DICT_VALUE = false 78 | DEDENT_CLOSING_BRACKETS = true 79 | EACH_DICT_ENTRY_ON_SEPARATE_LINE = true 80 | COALESCE_BRACKETS = true 81 | USE_TABS = false 82 | ALLOW_MULTILINE_LAMBDAS = true 83 | BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = true 84 | INDENT_DICTIONARY_VALUE = true 85 | SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN = true 86 | 87 | [aliases] 88 | test=pytest 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Define the gitcoin python api client setup configuration.""" 3 | from codecs import open 4 | from os import path 5 | 6 | from setuptools import find_packages, setup 7 | 8 | here = path.abspath(path.dirname(__file__)) 9 | 10 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='gitcoin', 15 | version='0.1.0', 16 | description='The Gitcoin.co python API client', 17 | long_description=long_description, 18 | long_description_content_type='text/markdown', 19 | url='https://github.com/gitcoinco/python-api-client', 20 | author='Gitcoin', 21 | author_email='team@gitcoin.co', 22 | classifiers=[ 23 | 'Development Status :: 3 - Alpha', 24 | 'Intended Audience :: Developers', 25 | 'Topic :: Software Development', 26 | 'Topic :: Software Development :: Libraries', 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'Topic :: Software Development :: Build Tools', 29 | 'License :: OSI Approved :: GNU Affero General Public License v3', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3 :: Only', 35 | 'Natural Language :: English', 36 | ], 37 | keywords='gitcoin api client bounties bounty rest', 38 | packages=find_packages(exclude=['docs', 'tests']), 39 | python_requires='~=3.5', 40 | install_requires=['requests'], 41 | setup_requires=['pytest-runner'], 42 | tests_require=['pytest', 'pytest-isort', 'pytest-cov', 'coverage', 'isort', 'responses'], 43 | extras_require={ 44 | 'deploy': ['twine', 'wheel'], 45 | }, 46 | project_urls={ 47 | 'Bug Reports': 'https://github.com/gitcoinco/python-api-client/issues', 48 | 'Homepage': 'https://gitcoin.co', 49 | 'Source': 'https://github.com/gitcoinco/python-api-client/', 50 | 'API Project': 'https://github.com/gitcoinco/web', 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitcoinco/python-api-client/462c3e4ac8a18dffd333fb1a9297bab4fe04a026/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_dry_run.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | import pytest 4 | import requests 5 | import requests.exceptions 6 | import responses 7 | from gitcoin import BountyConfig, Gitcoin 8 | 9 | 10 | def are_url_queries_equal(url1, url2, *more_urls): 11 | queries = [] 12 | urls = [url1, url2] 13 | urls.extend(more_urls) 14 | for url in urls: 15 | query_string = urllib.parse.urlparse(url).query 16 | query = urllib.parse.parse_qs(query_string) 17 | queries.append(query) 18 | for i in range(1, len(queries)): 19 | if not (queries[i - 1] == queries[i]): 20 | return False 21 | return True 22 | 23 | 24 | class TestGitcoinDryRun(): 25 | 26 | def test_are_url_queries_equal(self): 27 | assert are_url_queries_equal('https://google.com', 'https://google.com') 28 | assert not are_url_queries_equal('https://google.com?q=1', 'https://google.com?q=2') 29 | assert not are_url_queries_equal('https://google.com?q=1', 'https://google.com?q=2', 'https://google.com?q=3') 30 | assert not are_url_queries_equal( 31 | 'https://google.com?q=1', 'https://google.com?q=2', 'https://google.com?q=3', 'https://google.com?q=4' 32 | ) 33 | assert are_url_queries_equal('https://google.com?q=1', 'https://google.com?q=1', 'https://google.com?q=1') 34 | assert are_url_queries_equal( 35 | 'https://google.com?q=1', 'https://google.com?q=1', 'https://google.com?q=1', 'https://google.com?q=1' 36 | ) 37 | assert are_url_queries_equal('https://google.com?q=1&r=2', 'https://google.com?r=2&q=1') 38 | with pytest.raises(TypeError): 39 | are_url_queries_equal('https://google.com?q=1') 40 | 41 | def test_cfg_raises_on_unknown_param(self): 42 | cfg = BountyConfig() 43 | with pytest.raises(KeyError): 44 | cfg.get('does_not_exist') 45 | 46 | def test_api_raises_on_unknown_param(self): 47 | api = Gitcoin() 48 | with pytest.raises(KeyError): 49 | api.bounties.filter(does_not_exist=True) 50 | 51 | @responses.activate 52 | def test_all(self): 53 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 54 | api = Gitcoin() 55 | result = api.bounties.all() 56 | assert result == {'mock': 'mock'} 57 | assert len(responses.calls) == 1 58 | assert are_url_queries_equal(responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/') 59 | 60 | @responses.activate 61 | def test_filter_pk__gt(self): 62 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 63 | api = Gitcoin() 64 | result = api.bounties.filter(pk__gt=100).all() 65 | assert result == {'mock': 'mock'} 66 | assert len(responses.calls) == 1 67 | assert are_url_queries_equal(responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/?pk__gt=100') 68 | 69 | @responses.activate 70 | def test_filter_experience_level(self): 71 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 72 | api = Gitcoin() 73 | result = api.bounties.filter(experience_level='Beginner').all() 74 | assert result == {'mock': 'mock'} 75 | assert len(responses.calls) == 1 76 | assert are_url_queries_equal( 77 | responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/?experience_level=Beginner' 78 | ) 79 | with pytest.raises(ValueError): 80 | api.bounties.filter(experience_level='Rockstar') 81 | 82 | @responses.activate 83 | def test_filter_project_length(self): 84 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 85 | api = Gitcoin() 86 | result = api.bounties.filter(project_length='Hours').all() 87 | assert result == {'mock': 'mock'} 88 | assert len(responses.calls) == 1 89 | assert are_url_queries_equal( 90 | responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/?project_length=Hours' 91 | ) 92 | with pytest.raises(ValueError): 93 | api.bounties.filter(project_length='Minutes') 94 | 95 | @responses.activate 96 | def test_filter_bounty_type(self): 97 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 98 | api = Gitcoin() 99 | result = api.bounties.filter(bounty_type='Bug').all() 100 | assert result == {'mock': 'mock'} 101 | assert len(responses.calls) == 1 102 | assert are_url_queries_equal( 103 | responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/?bounty_type=Bug' 104 | ) 105 | with pytest.raises(ValueError): 106 | api.bounties.filter(bounty_type='Fancy') 107 | 108 | @responses.activate 109 | def test_filter_idx_status(self): 110 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 111 | api = Gitcoin() 112 | result = api.bounties.filter(idx_status='started').all() 113 | assert result == {'mock': 'mock'} 114 | assert len(responses.calls) == 1 115 | assert are_url_queries_equal( 116 | responses.calls[0].request.url, 'https://gitcoin.co/api/v0.1/bounties/?idx_status=started' 117 | ) 118 | with pytest.raises(ValueError): 119 | api.bounties.filter(idx_status='undone') 120 | 121 | @responses.activate 122 | def test_filter_2x_bounty_type_paged(self): 123 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 124 | api = Gitcoin() 125 | result = api.bounties.filter(bounty_type='Feature').filter(bounty_type='Bug').get_page() 126 | assert result == {'mock': 'mock'} 127 | assert len(responses.calls) == 1 128 | assert are_url_queries_equal( 129 | responses.calls[0].request.url, 130 | 'https://gitcoin.co/api/v0.1/bounties/?bounty_type=Feature%2CBug&offset=0&limit=25' 131 | ) 132 | 133 | @responses.activate 134 | def test_del_param(self): 135 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 136 | api = Gitcoin() 137 | result = api.bounties.filter(bounty_type='Feature') \ 138 | ._del_param('bounty_type').filter(bounty_type='Bug').get_page() 139 | assert result == {'mock': 'mock'} 140 | assert len(responses.calls) == 1 141 | assert are_url_queries_equal( 142 | responses.calls[0].request.url, 143 | 'https://gitcoin.co/api/v0.1/bounties/?bounty_type=Bug&offset=0&limit=25' 144 | ) 145 | 146 | @responses.activate 147 | def test_reset_all_params(self): 148 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 149 | api = Gitcoin() 150 | bounties_api = api.bounties 151 | 152 | result = bounties_api.filter(bounty_type='Feature').get_page() 153 | assert result == {'mock': 'mock'} 154 | assert len(responses.calls) == 1 155 | assert are_url_queries_equal( 156 | responses.calls[0].request.url, 157 | 'https://gitcoin.co/api/v0.1/bounties/?bounty_type=Feature&offset=0&limit=25' 158 | ) 159 | 160 | bounties_api._reset_all_params() 161 | 162 | result = bounties_api.filter(bounty_type='Bug').get_page() 163 | assert result == {'mock': 'mock'} 164 | assert len(responses.calls) == 2 165 | assert are_url_queries_equal( 166 | responses.calls[1].request.url, 'https://gitcoin.co/api/v0.1/bounties/?bounty_type=Bug&offset=0&limit=25' 167 | ) 168 | 169 | @responses.activate 170 | def test_order_by(self): 171 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 172 | api = Gitcoin() 173 | 174 | result = api.bounties.order_by('-project_length').get_page() 175 | assert result == {'mock': 'mock'} 176 | assert len(responses.calls) == 1 177 | assert are_url_queries_equal( 178 | responses.calls[0].request.url, 179 | 'https://gitcoin.co/api/v0.1/bounties/?order_by=-project_length&offset=0&limit=25' 180 | ) 181 | 182 | result = api.bounties.order_by('is_open').get_page() 183 | assert result == {'mock': 'mock'} 184 | assert len(responses.calls) == 2 185 | assert are_url_queries_equal( 186 | responses.calls[1].request.url, 187 | 'https://gitcoin.co/api/v0.1/bounties/?order_by=is_open&offset=0&limit=25' 188 | ) 189 | 190 | with pytest.raises(ValueError): 191 | api.bounties.order_by('random') 192 | 193 | @responses.activate 194 | def test_get(self): 195 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/123', json={'mock': 'mock'}, status=200) 196 | api = Gitcoin() 197 | result = api.bounties.get(123) 198 | assert result == {'mock': 'mock'} 199 | assert len(responses.calls) == 1 200 | responses.calls[0].request.url == 'https://gitcoin.co/api/v0.1/bounties/123' 201 | 202 | @responses.activate 203 | def test_no_normalize(self): 204 | 205 | class ExtendedBountyConfig(BountyConfig): 206 | 207 | def __init__(self): 208 | super().__init__() 209 | self.params['no_normalize'] = (True, None) 210 | 211 | api = Gitcoin() 212 | api.set_class('bounties_list_config', ExtendedBountyConfig) 213 | 214 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=200) 215 | 216 | result = api.bounties.filter(no_normalize='not_normal').get_page() 217 | assert result == {'mock': 'mock'} 218 | assert len(responses.calls) == 1 219 | assert are_url_queries_equal( 220 | responses.calls[0].request.url, 221 | 'https://gitcoin.co/api/v0.1/bounties/?no_normalize=not_normal&offset=0&limit=25' 222 | ) 223 | 224 | @responses.activate 225 | def test_raise_for_status(self): 226 | responses.add(responses.GET, 'https://gitcoin.co/api/v0.1/bounties/', json={'mock': 'mock'}, status=401) 227 | api = Gitcoin() 228 | with pytest.raises(requests.exceptions.HTTPError): 229 | result = api.bounties.all() 230 | 231 | def test_extending_config_does_not_leak(self): 232 | 233 | class ExtendedBountyConfig(BountyConfig): 234 | 235 | def __init__(self): 236 | super().__init__() 237 | self.params['extra_config'] = (True, None) 238 | 239 | extended_bounty_config = ExtendedBountyConfig() 240 | normal_bounty_config = BountyConfig() 241 | with pytest.raises(KeyError): 242 | normal_bounty_config.get('extra_config') 243 | -------------------------------------------------------------------------------- /tests/test_live.py: -------------------------------------------------------------------------------- 1 | import gitcoin.validation 2 | import pytest 3 | from gitcoin import BountyConfig, Gitcoin 4 | 5 | 6 | def assert_is_list_of_bounties(result): 7 | assert list == type(result) 8 | for bounty in result: 9 | assert_is_bounty(bounty) 10 | 11 | 12 | def assert_is_bounty(bounty): 13 | assert isinstance(int, bounty['pk']) 14 | assert bounty['pk'] > 0 15 | 16 | 17 | @pytest.mark.skipif( 18 | not pytest.config.getoption('--liveapi'), 19 | reason='Please only test against the live API manually by specifying --live-api.' 20 | ) 21 | class TestGitcoinLiveBounties(): 22 | 23 | filter_examples = { 24 | 'experience_level': ['Beginner', 'Advanced', 'Intermediate', 'Unknown'], 25 | 'project_length': ['Hours', 'Days', 'Weeks', 'Months', 'Unknown'], 26 | 'bounty_type': ['Bug', 'Security', 'Feature', 'Unknown'], 27 | 'bounty_owner_address': ['0x4331b095bc38dc3bce0a269682b5ebaefa252929'], 28 | 'bounty_owner_github_username': ['owocki'], 29 | 'idx_status': ['cancelled', 'done', 'expired', 'open', 'started', 'submitted', 'unknown'], 30 | 'network': ['mainnet', 'rinkeby'], 31 | 'standard_bounties_id': [45, 215], 32 | 'pk__gt': [3270], 33 | 'started': ['owocki'], 34 | 'is_open': [True, False], 35 | 'github_url': ['https://github.com/gitcoinco/web/issues/805'], 36 | 'fulfiller_github_username': ['owocki'], 37 | 'interested_github_username': ['owocki'], 38 | } 39 | 40 | def test_filter_examples(self): 41 | api = Gitcoin() 42 | for filter_name, examples in self.filter_examples.items(): 43 | for example in examples: 44 | filter_kwargs = {filter_name: example} 45 | # try: 46 | result = api.bounties.filter(**filter_kwargs).get_page(per_page=1) 47 | # except 48 | assert_is_list_of_bounties(result) 49 | 50 | def test_multiple_value_filters(self): 51 | api = Gitcoin() 52 | cfg = BountyConfig() 53 | for filter_name, examples in self.filter_examples.items(): 54 | is_multiple, _ = cfg.get(filter_name) 55 | if is_multiple: 56 | bounties = api.bounties 57 | for example in examples: 58 | filter_kwargs = {filter_name: example} 59 | bounties.filter(**filter_kwargs) 60 | result = bounties.get_page(per_page=1) 61 | assert_is_list_of_bounties(result) 62 | 63 | def test_order_by(self): 64 | sort_field_names = gitcoin.validation.OPTIONS['order_by'] 65 | api = Gitcoin() 66 | for field_name in sort_field_names: 67 | for direction in [field_name, ''.join(('-', field_name))]: 68 | result = api.bounties.order_by(direction).get_page(per_page=1) 69 | assert_is_list_of_bounties(result) 70 | --------------------------------------------------------------------------------