diff --git a/.gitignore b/.gitignore
index 70fedcd..afda2b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,15 @@
+# ide settings
.idea/
+.vscode/
+
+# virtual environment
+venv/
+
+# python build stuff
__pycache__/
-*.xml
-*.pyc
-autorequests.egg-info/
-build/
+*.py[cod]
+*$py.class
dist/
-reinstall.bat
-build.bat
\ No newline at end of file
+
+# pytest cache
+.pytest_cache/
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..2e32ab1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,7 @@
+## 🐞 Contributing
+
+This project has a lot of room for improvement in optimizing regexps, better OOP, bug fixes, and completing todos.
+If you make an issue, pr, or suggestion, it'll be very appreciated <3.
+
+I'd prefer if you opened an issue before making a pull request with new features,
+but if you find a bug and want to fix it, that can be opened whenever :))
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index a612ad9..bf153e6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,373 +1,21 @@
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
+MIT License
+
+Copyright (c) 2021 Hexiro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
index bcdca02..8229ef7 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
-# autorequests
+# AutoRequests
-Autorequests provides an easy way to create a simple API wrapper from request data generated by your browser.
+AutoRequests provides an easy way to create a simple API wrapper from request data generated by your browser.
![BUILT WITH SWAG](https://forthebadge.com/images/badges/built-with-swag.svg)
![NOT A BUG A FEATURE](https://forthebadge.com/images/badges/not-a-bug-a-feature.svg)
![IT WORKS. WHY?](https://forthebadge.com/images/badges/it-works-why.svg)
-### Showcase
+### 📺 Demo
** the website shown in this example is [imperialb.in](https://imperialb.in)
![example showcase gif](https://i.imgur.com/75tMMIW.gif)
-### Example Use Cases
+### 💼 Example Use Cases
* Creating a foundation for an API wrapper
* Recreating a request outside the browser
@@ -19,30 +19,27 @@ Autorequests provides an easy way to create a simple API wrapper from request da
### ✂️ How to Copy
-1. Inspect Element
-2. Go to `Network` tab
-3. Find web request
-4. Right-Click
-5. Copy
-6. Choose A `Copying Method`:
-
-### Supported Copying Methods
-
-* Powershell
-* Node.JS fetch
+1. Inspect Element
+2. Go to `Network` tab
+3. Find web request
+4. Right-Click
+5. Copy
+6. Choose one of the following:
+ 1. Powershell
+ 2. Node.JS fetch
## 📦 Installation
install the package with pip
```
-$ pip3 install autorequests
+$ pip install autorequests
```
or download the latest development build from GitHub
```
-$ pip3 install -U git+https://github.com/Hexiro/autorequests
+$ pip install -U git+https://github.com/Hexiro/autorequests
```
## 🖥️ Command Line
@@ -62,10 +59,8 @@ generation options
```
--return-text Makes the generated method's responses return .text instead of .json()
- --single-quote Uses single quotes instead of double quotes
--no-headers Removes all headers from the operation
--no-cookies Removes all cookies from the operation
- --compare Compares the previously generated files to the new files
--parameters Replaces hardcoded params, json, data, etc with parameters that have default values
```
@@ -73,14 +68,18 @@ generation options
* Method names are parsed from the url, but if the URL doesn't have any paths with a valid method name, an invalid
method name will be used.
-* Sometimes when copying fetches from the browser, some important headers aren't including, causing the resulting API
+* Sometimes when copying from the browser, important headers aren't included which causes the resulting API
wrapper to fail requests.
+* Parsing multipart/form-data when copying with the powershell mode isn't supported
-## 📅 Planned Features
+## 🐞 Contributing
-* detecting base paths (like /api/v1) and setting that in the class constructor. (maybe).
+see [CONTRIBUTING.md](./CONTRIBUTING.md)
-## 🐞 Contributing
-This project has a lot of room for improvement in optimizing regexps, better OOP, and bug fixes. If you make an issue,
-pr, or suggestion, it'll be very appreciated <3.
\ No newline at end of file
+## 📅 # TODO
+* unit tests w/ pytest
+* clean up cli
+* replace ***--compare*** with ***--diff*** which shows diffs in console
+* better output w/ rich
+* possibly decouple method & class ?
\ No newline at end of file
diff --git a/autorequests/__init__.py b/autorequests/__init__.py
index 50f46ab..7f76a93 100644
--- a/autorequests/__init__.py
+++ b/autorequests/__init__.py
@@ -1,3 +1,4 @@
+from .__main__ import AutoRequests, main, __version__
+
__name__ = "autorequests"
-__version__ = "1.0.2"
__description__ = "Automatically create a simple API wrapper from request data generated by your browser."
diff --git a/autorequests/__main__.py b/autorequests/__main__.py
index 8a31087..6bee37a 100644
--- a/autorequests/__main__.py
+++ b/autorequests/__main__.py
@@ -1,184 +1,221 @@
import argparse
from pathlib import Path
-from typing import List
-
-from autorequests.classes.outputfile import OutputFile
-from .classes import Class, InputFile
-from .utils import PathType
-
-
-class AutoRequests(argparse.ArgumentParser):
- # filepath: PathType
- # filename: str
- # file: File
-
- def __init__(self):
- super().__init__()
- self.add_argument("-i", "--input", default=None, help="Input Directory")
- self.add_argument("-o", "--output", default=None, help="Output Directory")
- self.add_argument("--return-text", action="store_true",
- help="Makes the generated method's responses return .text instead of .json()"
- )
- self.add_argument("--single-quote", action="store_true", help="Uses single quotes instead of double quotes")
- self.add_argument("--no-headers", action="store_true", help="Removes all headers from the operation")
- self.add_argument("--no-cookies", action="store_true", help="Removes all cookies from the operation")
- self.add_argument("--compare", action="store_true",
- help="Compares the previously generated files to the new files."
- )
- self.add_argument("--parameters",
- action="store_true",
- help="Replaces hardcoded params, json, data, etc with parameters that have default values")
- args = self.parse_args()
-
- # resolves path
- self.__input = (Path(args.i) if args.input else Path.cwd()).resolve()
- self.__output = (Path(args.o) if args.output else Path.cwd()).resolve()
- self.__single_quote = args.single_quote
- self.__return_text = args.return_text
- self.__no_headers = args.no_headers
- self.__no_cookies = args.no_cookies
- self.__compare = args.compare
- self.__parameters_mode = args.parameters
-
- # dynamic tings from here on out
- self.__classes = []
- self.__input_files = []
- self.__output_files = []
- self.__has_written = False
+from typing import List, Optional, Dict, Generator
- @property
- def input(self) -> PathType:
- return self.__input
+import rich
+from rich.box import MINIMAL
+from rich.table import Table
- @property
- def output(self) -> PathType:
- return self.__output
+from .classes import Class, Method
+from .utilities import cached_property
+from .utilities.inspector import inspect
+from .utilities.parsing import method_from_text
- @property
- def single_quote(self) -> bool:
- return self.__single_quote
+__version__ = "1.1.0"
+__all__ = (
+ "AutoRequests",
+ "main",
+ "__version__"
+)
+
+console = rich.get_console()
+
+
+class AutoRequests:
+
+ def __init__(self, *,
+ input_path: Path,
+ output_path: Path,
+ return_text: bool = False,
+ no_headers: bool = False,
+ no_cookies: bool = False,
+ parameters: bool = False
+ ):
+
+ # params
+
+ self._return_text: bool = return_text
+ self._no_headers: bool = no_headers
+ self._no_cookies: bool = no_cookies
+ self._parameters: bool = parameters
+
+ # dynamic
+ self._input_path: Path = input_path
+ self._output_path: Path = output_path
+ self._input_methods: Dict[Path, Method] = {}
+ self._output_classes: Dict[Path, Class] = {}
+
+ self._methods: List[Method] = self.methods_from_path(self.input_path)
+ self._classes: List[Class] = \
+ [Class(name=name, output_path=output_path, return_text=return_text, no_headers=no_headers,
+ no_cookies=no_cookies, parameters=parameters)
+ for name in {method.class_name for method in self.methods}]
+
+ for cls in self.classes:
+ if cls.folder != self.output_path:
+ self.methods.extend(self.methods_from_path(cls.folder))
+
+ for method in self.methods:
+ cls = self.find_class(method.class_name)
+ cls.add_method(method)
+ method.class_ = cls
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} classes={self.classes!r}>"
@property
def return_text(self) -> bool:
- return self.__return_text
+ return self._return_text
@property
def no_headers(self) -> bool:
- return self.__no_headers
+ return self._no_headers
@property
def no_cookies(self) -> bool:
- return self.__no_cookies
+ return self._no_cookies
@property
- def compare(self) -> bool:
- return self.__compare
+ def parameters(self) -> bool:
+ return self._parameters
@property
- def parameters_mode(self) -> bool:
- return self.__parameters_mode
-
- # dynamic
+ def input_path(self) -> Path:
+ return self._input_path
@property
- def classes(self) -> List[Class]:
- return self.__classes
+ def output_path(self) -> Path:
+ return self._output_path
@property
- def input_files(self) -> List[InputFile]:
- return self.__input_files
+ def input_methods(self):
+ return self._input_methods
@property
- def output_files(self) -> List[OutputFile]:
- return self.__output_files
+ def output_classes(self):
+ return self._output_classes
- @property
- def has_written(self) -> bool:
- return self.__has_written
+ @cached_property
+ def methods(self) -> List[Method]:
+ return self._methods
- def load_local_files(self):
- self.parse_directory(self.input)
+ @cached_property
+ def classes(self) -> List[Class]:
+ return self._classes
- def load_external_files(self):
- for output_file in self.output_files:
- if not output_file.in_same_dir():
- if output_file.folder.is_dir():
- self.parse_directory(output_file.folder)
- else:
- output_file.folder.mkdir()
+ def class_output_path(self, cls: Class):
+ if self.output_path.name != cls.name:
+ return self.output_path / cls.name
- def write(self):
- if self.has_written:
- return
+ return cls
- for output_file in self.output_files:
- # need to check changes before writing again
- if self.compare and output_file.python_file.is_file():
- output_file.write_changes()
- output_file.write()
+ def methods_from_path(self, path: Path) -> List[Method]:
+ methods = []
+ for file in self.files_from_path(path):
+ text = file.read_text(encoding="utf8", errors="ignore")
+ method = method_from_text(text)
+ if method is None:
+ continue
+ methods.append(method)
+ self.input_methods[file] = method
+ return methods
- self.__has_written = True
+ @property
+ def top(self):
+ return ("import requests\n"
+ "\n"
+ "\n"
+ "# Automatically generated by https://github.com/Hexiro/autorequests.\n"
+ "\n")
- def move_into_class_folder(self):
- for file in self.input_files:
- class_name = file.method.class_name
- if self.output.name != class_name:
- file.rename(self.output / class_name / file.name)
+ def write(self):
+ for cls in self.classes:
+ if not cls.folder.exists():
+ cls.folder.mkdir()
+ main_py = cls.folder / "main.py"
+ main_py.write_text(data=self.top + cls.code, encoding="utf8", errors="strict")
+ self.output_classes[main_py] = cls
+ for file, method in self.input_methods.items():
+ class_name = method.class_name
+ if self.output_path.name != class_name:
+ file.rename(self.output_path / class_name / file.name)
+
+ @staticmethod
+ def files_from_path(path: Path) -> Generator[Path, None, None]:
+ return path.glob("*.txt")
+
+ def find_class(self, name: str) -> Optional[Class]:
+ return next((cls for cls in self.classes if cls.name == name), None)
+
+ def main(self):
+ self.write()
+ self.print_results()
def print_results(self):
- if len(self.classes) == 0:
+ if not self.output_classes:
print("No request data could be located.")
return
- if not self.has_written:
- print("Modules haven't been written to the filesystem yet.")
- return
- num_classes = len(self.classes)
- num_methods = len(self.input_files)
- classes_noun = "classes" if num_classes > 1 else "class"
- methods_noun = "methods" if num_methods > 1 else "method"
- print(f"Successfully wrote {num_classes} {classes_noun} with a total of {num_methods} {methods_noun}.")
-
- def parse_directory(self, directory: Path):
- if not directory.is_dir():
- return
- for filename in directory.glob("*.txt"):
- file = InputFile(filename)
- method = file.method
- if method:
- class_name = file.method.class_name
- classes_search = [c for c in self.classes if c.name == class_name]
-
- if not classes_search:
- class_object = Class(name=class_name,
- return_text=self.return_text,
- single_quote=self.single_quote,
- parameters_mode=self.parameters_mode)
- self.classes.append(class_object)
- self.output_files.append(OutputFile(self.output, class_object))
- else:
- class_object = classes_search[0]
-
- # needs to be added first
- # modifying methods after adding it to the class is perfectly fine
-
- class_object.add_method(method)
- self.input_files.append(file)
-
- # maybe this could be optimized?
- # cpu is wasted calculating headers and cookies only to be deleted
- if self.no_headers:
- file.method.headers = {}
- if self.no_cookies:
- file.method.cookies = {}
+ table = Table(box=MINIMAL, border_style="bold red")
+ code = []
+ for path, cls in self.output_classes.items():
+ # p.s. if you try and make an object, python will throw an error because `requests` isn't defined
+ # thankfully, the class can be created which is pretty cool (thanks interpreter :))
+
+ try:
+ try:
+ exec(cls.code)
+ except SyntaxError as err:
+ # "invalid syntax in the code generated. is this worth reporting?"
+ err.msg += " in the code generated. is this worth reporting?"
+ raise
+ except SyntaxError:
+ console.print_exception()
+ return
+
+ name = path.parent.name
+ table.add_column(f"[bold red]{name}[/bold red]")
+ generated_cls = eval(cls.name)
+ code.append(inspect(generated_cls))
+ table.width = 65 * len(code)
+ table.add_row(*code)
+ console.print(table)
def main():
- auto_requests = AutoRequests()
- auto_requests.load_local_files()
- auto_requests.load_external_files()
- auto_requests.write()
- auto_requests.move_into_class_folder()
- auto_requests.print_results()
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-i", "--input", default=None, help="Input Directory")
+ parser.add_argument("-o", "--output", default=None, help="Output Directory")
+ parser.add_argument("-v", "--version", action="store_true")
+ parser.add_argument("--return-text",
+ action="store_true",
+ help="Makes the generated method's responses return .text instead of .json()"
+ )
+ parser.add_argument("--no-headers", action="store_true", help="Removes all headers from the operation")
+ parser.add_argument("--no-cookies", action="store_true", help="Removes all cookies from the operation")
+ parser.add_argument("--parameters",
+ action="store_true",
+ help="Replaces hardcoded params, json, data, etc with parameters that have default values")
+ args = parser.parse_args()
+
+ if not args:
+ parser.print_help()
+ return
+ if args.version:
+ print(f"AutoRequests {__version__}")
+ return
+
+ input_path = (Path(args.input) if args.input else Path.cwd()).resolve()
+ output_path = (Path(args.output) if args.output else Path.cwd()).resolve()
+
+ auto_requests = AutoRequests(
+ input_path=input_path,
+ output_path=output_path,
+ return_text=args.return_text,
+ no_headers=args.no_headers,
+ no_cookies=args.no_cookies,
+ parameters=args.parameters
+ )
+ auto_requests.main()
if __name__ == "__main__":
diff --git a/autorequests/classes/__init__.py b/autorequests/classes/__init__.py
index 045a218..c558bcd 100644
--- a/autorequests/classes/__init__.py
+++ b/autorequests/classes/__init__.py
@@ -5,10 +5,8 @@
# doesn't import from __init__.py
from .body import Body
-from .case import Case
-from .class_ import Class
from .parameter import Parameter
from .url import URL
# imports from __init__.py
+from .class_ import Class
from .method import Method
-from .inputfile import InputFile
diff --git a/autorequests/classes/body.py b/autorequests/classes/body.py
index 2f2f417..185daea 100644
--- a/autorequests/classes/body.py
+++ b/autorequests/classes/body.py
@@ -1,6 +1,6 @@
import json
import urllib.parse
-from typing import Optional
+from typing import Optional, Dict, Tuple, Union
class Body:
@@ -10,25 +10,31 @@ def __init__(self, body: Optional[str]):
# parse escape sequences :thumbs_up:
# ignore/replace are kind of just guesses at what i think would be best
# if there is a more logical reason to use something else LMK!
- body = (body.encode(encoding="utf8", errors="ignore")
+ body = (body
+ .encode(encoding="utf8", errors="ignore")
.decode(encoding="unicode_escape", errors="replace"))
# replace line breaks with \n(s)
body = "\n".join(body.splitlines())
- self.__body = body
- self.__data = {}
- self.__json = {}
- self.__files = {}
+ self._body: Optional[str] = body
+ self._data: Dict[str, str] = {}
+ self._json: Dict[str, str] = {}
+ # tuple of four items
+ # 1. filename
+ # 2. content
+ # 3. content-type
+ # 4. extra headers
+ self._files: Dict[str, Union[Tuple[str, str], Tuple[str, str, str]]] = {}
# multipart is the most broad and obvious so it goes first
if not body:
pass
elif self.is_multipart_form_data:
- self.__parse_multipart_form_data()
+ self._parse_multipart_form_data()
elif self.is_json:
- self.__parse_json()
+ self._parse_json()
# urlencoded is the simplest (hardest to check) so it goes last
elif self.is_urlencoded:
- self.__parse_urlencoded()
+ self._parse_urlencoded()
def __repr__(self):
base = "
bool:
- """
- :returns: true if text is a single lowercase word
- that could be considered any case convention really (besides pascal ig)
- """
- return self.text.islower() and uses_accepted_chars(self.text, string.ascii_lowercase)
-
- @cached_property
- def is_snake_case(self) -> bool:
- return self.text.islower() and uses_accepted_chars(self.text, self.snake_case_chars)
-
- @cached_property
- def is_kebab_case(self) -> bool:
- return self.text.islower() and uses_accepted_chars(self.text, self.kebab_case_chars)
-
- @cached_property
- def is_dot_case(self) -> bool:
- return self.text.islower() and uses_accepted_chars(self.text, self.dot_case_chars)
-
- @cached_property
- def is_camel_case(self) -> bool:
- return self.text[0].islower() and not self.text.islower() and \
- uses_accepted_chars(self.text, self.camel_case_chars)
-
- @property
- def is_pascal_case(self) -> bool:
- return self.text[0].isupper() and uses_accepted_chars(self.text, self.pascal_case_chars)
-
- @staticmethod
- def camel_to_snake(text: str) -> str:
- return "".join("_" + t.lower() if t.isupper() else t for t in text).lstrip("_")
-
- @classmethod
- def pascal_to_snake(cls, text: str) -> str:
- # pascal and camel case both work here
- return cls.camel_to_snake(text)
-
- @staticmethod
- def kebab_to_snake(text: str) -> str:
- return text.replace("-", "_")
-
- @staticmethod
- def dot_to_snake(text: str) -> str:
- return text.replace(".", "_")
-
- @staticmethod
- def snake_to_camel(text: str) -> str:
- return "".join(t.lower() if i == 0 else t.capitalize() for i, t in enumerate(text.split("_")))
-
- @staticmethod
- def snake_to_pascal(text: str) -> str:
- return "".join(t.capitalize() for t in text.split("_"))
-
- @staticmethod
- def snake_to_kebab(text: str) -> str:
- return text.replace("_", "-").lower()
-
- @staticmethod
- def snake_to_dot(text: str) -> str:
- return text.replace("_", ".").lower()
diff --git a/autorequests/classes/class_.py b/autorequests/classes/class_.py
index 09747c5..628edfa 100644
--- a/autorequests/classes/class_.py
+++ b/autorequests/classes/class_.py
@@ -1,61 +1,88 @@
-from ..utils import format_dict, indent, unique_name, compare_dicts
+from pathlib import Path
+from typing import List, Dict
+from . import method
+from ..utilities import format_dict, indent, compare_dicts, cached_property
-# "class" is a reserved keyword so I can't name a file "class"
+# "class" is a reserved keyword so I can't name a file "class"
class Class:
- def __init__(self, name: str,
+ def __init__(self,
+ name: str,
+ output_path: Path,
return_text: bool = False,
- single_quote: bool = False,
- parameters_mode: bool = False):
- self.__name = name
- self.__methods = []
- self.__cookies = {}
- self.__headers = {}
-
- self.__return_text = return_text
- self.__single_quote = single_quote
- self.__parameters_mode = parameters_mode
+ no_headers: bool = False,
+ no_cookies: bool = False,
+ parameters: bool = False):
+ self._name: str = name
+ self._output_path: Path = output_path
+ self._methods: List["method.Method"] = []
+ self._cookies: Dict[str, str] = {}
+ self._headers: Dict[str, str] = {}
+
+ self._return_text: bool = return_text
+ self._no_headers: bool = no_headers
+ self._no_cookies: bool = no_cookies
+ self._parameters: bool = parameters
def __repr__(self):
return f""
@property
def name(self):
- return self.__name
+ return self._name
+
+ @property
+ def output_path(self):
+ return self._output_path
@property
def methods(self):
- return self.__methods
+ return self._methods
@property
def headers(self):
- return self.__headers
+ return self._headers
@property
def cookies(self):
- return self.__cookies
+ return self._cookies
@property
def return_text(self):
- return self.__return_text
+ return self._return_text
+
+ @property
+ def parameters(self):
+ return self._parameters
+
+ @property
+ def no_headers(self):
+ return self._no_headers
@property
- def single_quote(self):
- return self.__single_quote
+ def no_cookies(self):
+ return self._no_cookies
+
+ @cached_property
+ def folder(self) -> Path:
+ if self.output_path.name != self.name:
+ return self.output_path / self.name
+ # ex. class is named "autorequests" and output folder is named "autorequests"
+ return self.output_path
@property
- def parameters_mode(self):
- return self.__parameters_mode
+ def file(self):
+ return self.folder / "main.py"
@property
def signature(self):
return f"class {self.name}:"
@property
- def constructor(self):
+ def initializer(self):
signature = "def __init__(self):\n"
code = "self.session = requests.Session()\n"
if self.headers:
@@ -66,44 +93,32 @@ def constructor(self):
code += f"self.session.cookies.set(\"{cookie}\", \"{value}\")\n"
return signature + indent(code)
+ @property
+ def use_initializer(self) -> bool:
+ return bool(self.headers or self.cookies)
+
@property
def code(self):
code = self.signature
# not actually two newlines; adds \n to end of previous line
- if self.headers or self.cookies:
+ if self.use_initializer:
code += "\n\n"
- code += indent(self.constructor)
+ code += indent(self.initializer)
for method in self.methods:
code += "\n\n"
code += indent(method.code)
code += "\n"
- if self.single_quote:
- # replace unescaped 's with escaped 's
- code = code.replace("'", "\\'")
- # replace escaped "s with escaped 's
- code = code.replace("\\\"", "\\'")
- # replace all "s with 's
- code = code.replace("\"", "'")
return code
- def add_method(self, method):
- """
- :type method: Method
- """
+ def add_method(self, method: "method.Method"):
method.class_ = self
-
- # there will only ever be one time where there are two methods with the same name,
- # and this right checks that and adds a _one after it
- # the unique_name function on the bottom will add a _two to that one, and so on.
-
- for old_method in self.methods:
- if old_method.name == method.name:
- old_method.name = old_method.name + "_one"
- break
-
- # this line showcases 3 instances of 'method.name' LOL
- method.name = unique_name(method.name, [method.name for method in self.methods])
- self.__methods.append(method)
+ method.ensure_unique_name()
+ self._methods.append(method)
if len(self.methods) >= 2:
- self.__headers = compare_dicts([method.headers for method in self.methods])
- self.__cookies = compare_dicts([method.cookies for method in self.methods])
+ self._headers = compare_dicts(*(method.all_headers for method in self.methods))
+ self._cookies = compare_dicts(*(method.all_cookies for method in self.methods))
+
+ if self.no_headers:
+ method.remove_headers()
+ if self.no_cookies:
+ method.remove_cookies()
diff --git a/autorequests/classes/inputfile.py b/autorequests/classes/inputfile.py
deleted file mode 100644
index 2a43d22..0000000
--- a/autorequests/classes/inputfile.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import json
-from typing import Match
-
-from .. import regexp
-from ..classes import URL, Body, Method
-from ..utils import extract_cookies, PathType, cached_property
-
-
-class InputFile(PathType):
- """ handles files and the parsing of files """
-
- @cached_property
- def text(self):
- return self.read_text(encoding="utf8", errors="ignore")
-
- @cached_property
- def method(self):
- fetch = self.fetch_match
- if fetch:
- return self._method_from_fetch(fetch)
- powershell = self.powershell_match
- if powershell:
- return self._method_from_powershell(powershell)
-
- @cached_property
- def fetch_match(self):
- return regexp.fetch_regexp.search(self.text)
-
- @cached_property
- def powershell_match(self):
- return regexp.powershell_regexp.search(self.text)
-
- # static methods
- # (for parsing)
-
- @staticmethod
- def _method_from_fetch(fetch: Match) -> Method:
- """
- Parses a file that follows this format:
-
- fetch(, {
- "headers": ,
- "referrer": , # optional
- "referrerPolicy": , # might be optional
- "body": ,
- "method": ,
- "mode":
- });
- """
- headers = json.loads(fetch["headers"])
- # referer is spelled wrong in the HTTP header
- # referrer policy is not
- referrer = fetch["referrer"]
- referrer_policy = fetch["referrer_policy"]
- if referrer:
- headers["referer"] = referrer
- if referrer_policy:
- headers["referrer-policy"] = referrer_policy
-
- cookies = extract_cookies(headers)
-
- method = fetch["method"]
- url = URL(fetch["url"])
- body = Body(fetch["body"])
-
- return Method(method=method,
- url=url,
- body=body,
- headers=headers,
- cookies=cookies,
- )
-
- @staticmethod
- def _method_from_powershell(powershell: Match) -> Method:
- """
- Parses a file that follows this format:
-
- Invoke-WebRequest -Uri `
- -Method ` # optional; defaults to GET if not set
- -Headers `
- -ContentType ` # optional; only exists w/ body
- -Body # optional; only exists if a body is present
- """
- headers = {}
-
- for header in powershell["headers"].splitlines():
- if "=" in header:
- header = header.lstrip(" ")
- key, value = header.split("=", maxsplit=1)
- # remove leading and trailing "s that always exist
- key = key[1:-1]
- value = value[1:-1]
- headers[key] = value
-
- cookies = extract_cookies(headers)
-
- # ` is the escape character in powershell
- # replace two `s with a singular `
- # replace singular `s with nothing
-
- raw_body = powershell["body"]
- if raw_body:
- raw_body = raw_body.split("`")
- raw_body = "".join((e if e != "" else "`") for e in raw_body)
-
- method = powershell["method"] or "GET"
- url = URL(powershell["url"])
- body = Body(raw_body)
-
- return Method(method=method,
- url=url,
- body=body,
- headers=headers,
- cookies=cookies,
- )
diff --git a/autorequests/classes/method.py b/autorequests/classes/method.py
index 7c32bc8..7f8e458 100644
--- a/autorequests/classes/method.py
+++ b/autorequests/classes/method.py
@@ -1,7 +1,9 @@
-from typing import List, Dict
+from typing import List, Dict, Optional
-from . import URL, Body, Parameter, Case
-from ..utils import format_dict, indent, is_pythonic_name, cached_property
+from . import URL, Body, Parameter
+from . import class_
+from ..utilities import format_dict, indent, is_pythonic_name, cached_property, unique_name, written_form
+from ..utilities.case import snake_case, pascal_case
class Method:
@@ -14,57 +16,59 @@ def __init__(self,
headers: Dict[str, str] = None,
cookies: Dict[str, str] = None
):
- self.__method = method
- self.__url = url
+ # request method (ex. GET, POST)
+ self._method: str = method
+ self._url: URL = url
# request body -- not to be confused with method body
- self.__body = body
+ self._body: Body = body
# must append to `parameters` property, and not __parameters
- self.__parameters = parameters
- self.__headers = headers or {}
- self.__cookies = cookies or {}
- self.__name = self.default_name
- self.__class = None
+ self._parameters: List[Parameter] = parameters or []
+ self._headers: Dict[str, str] = headers or {}
+ self._cookies: Dict[str, str] = cookies or {}
+ self._name: str = self.default_name
+ self._class: Optional["class_.Class"] = None
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{self.signature}>"
@property
- def class_(self):
- """
- :rtype: Class
- """
- return self.__class
+ def name(self) -> str:
+ return self._name
- @class_.setter
- def class_(self, new_class):
- """
- :type new_class: Class
- """
- self.__class = new_class
+ @name.setter
+ def name(self, new_name: str):
+ self._name = new_name
@property
- def class_headers(self) -> Dict[str, str]:
- return getattr(self.class_, "headers", {})
+ def method(self) -> str:
+ return self._method
@property
- def class_cookies(self) -> Dict[str, str]:
- return getattr(self.class_, "cookies", {})
+ def url(self) -> URL:
+ return self._url
@property
- def return_text(self) -> bool:
- return getattr(self.class_, "return_text", False)
+ def body(self) -> Body:
+ return self._body
+
+ @property
+ def signature(self) -> str:
+ return f"def {self.name}({', '.join(param.code for param in self.parameters)}):"
@property
- def parameters_mode(self) -> bool:
- return getattr(self.class_, "parameters_mode", False)
+ def class_(self) -> Optional["class_.Class"]:
+ return self._class
+
+ @class_.setter
+ def class_(self, new_class: "class_.Class"):
+ self._class = new_class
@cached_property
def parameters(self) -> List[Parameter]:
- params = [Parameter("self")]
- for param in (self.__parameters or []):
- if param.name != "self":
- params.append(param)
- if self.parameters_mode:
+ params = self._parameters
+ if not params or params[0].name != "self":
+ params.insert(0, Parameter("self"))
+ if self.class_ and self.class_.parameters:
for key, value in {**self.url.query,
**self.body.data,
**self.body.json,
@@ -72,49 +76,66 @@ def parameters(self) -> List[Parameter]:
params.append(Parameter(key, default=value))
return params
- @property
- def signature(self):
- return f"def {self.name}({', '.join(param.code for param in self.parameters)}):"
-
@cached_property
- def code(self):
- # handle class headers & cookies
+ def code(self) -> str:
# only use session if headers or cookies are set in class
- requests_call = "self.session" if (self.class_headers or self.class_cookies) else "requests"
- # code
- body = f"return {requests_call}.{self.method.lower()}(\"{self.url}\""
+ requests_call = "self.session" if self.class_ and self.class_.use_initializer else "requests"
+ body = f"{self.docstring}\nreturn {requests_call}.{self.method.lower()}(\"{self.url}\""
for kwarg, data in {"params": self.url.query,
"data": self.body.data,
"json": self.body.json,
"files": self.body.files,
"headers": self.headers,
"cookies": self.cookies}.items():
- if data:
- if self.parameters_mode:
- parameters_dict = {p.name: p for p in self.parameters}
- for key, value in data.items():
- data[key] = parameters_dict[key].name if key in parameters_dict else value
- formatted_data = format_dict(data, variables=[p.name for p in self.parameters])
- else:
- formatted_data = format_dict(data)
- body += f", {kwarg}=" + formatted_data
+ if not data:
+ continue
+ variables = None
+ if self.class_ and self.class_.parameters:
+ variables = [p.name for p in self.parameters]
+ for key, value in data.items():
+ data[key] = key if key in variables else value
+ body += f", {kwarg}=" + format_dict(data, variables=variables)
body += ")."
- body += "text" if self.return_text else "json()"
- return self.signature + "\n" + indent(body, spaces=4)
+ body += "text" if self.class_ and self.class_.return_text else "json()"
+ return self.signature + "\n" + indent(body)
+
+ @property
+ def docstring(self) -> str:
+ details: List[str] = []
+ for kwarg, data in {"param": self.url.query,
+ "data item": self.body.data,
+ "json item": self.body.json,
+ "file": self.body.files,
+ "header": self.headers,
+ "cookie": self.cookies}.items():
+ if not data:
+ continue
+ len_data = len(data)
+ # make plural
+ if len_data > 1:
+ kwarg += "s"
+ details.append(f"{len_data} {kwarg}")
+ if len(details) > 1:
+ details[-1] = f"and {details[-1]}."
+ details_string = ", ".join(details) if details else "no data."
+ return ("\"\"\"\n"
+ f"{self.method} {self.url}.\n"
+ f"Contains {details_string}\n"
+ "\"\"\"")
@cached_property
- def class_name(self):
+ def class_name(self) -> str:
# DOMAIN of url
# domains with two dots break this (ex. .co.uk)
class_name = self.url.domain.split(".")[-2]
# remove port
class_name = class_name.split(":")[0]
- return Case(class_name).camel_case
+ return pascal_case(written_form(class_name))
@cached_property
- def default_name(self):
+ def default_name(self) -> str:
# remove leading and trailing / for calculation
- split = [Case(p).snake_case for p in self.url.path.split("/") if p]
+ split = [snake_case(p) for p in self.url.path.split("/") if p]
split.reverse()
# find parts of path that meets python's syntax requirements for a method name
for part in split:
@@ -122,48 +143,44 @@ def default_name(self):
return part
# using base domain -- same name as class
if not split:
- return self.class_name
+ return snake_case(self.class_name)
# this will have an error in the generated code
- return split[-1]
-
- @property
- def headers(self):
- if self.class_headers:
- return {h: v for h, v in self.__headers.items() if h not in self.class_headers}
- return self.__headers
-
- @headers.setter
- def headers(self, new_headers: Dict[str, str]):
- if isinstance(new_headers, dict):
- self.__headers = new_headers
+ return snake_case(split[-1])
@property
- def cookies(self):
- if self.class_cookies:
- return {c: v for c, v in self.__headers.items() if c not in self.class_cookies}
- return self.__cookies
-
- @cookies.setter
- def cookies(self, new_cookies: Dict[str, str]):
- if isinstance(new_cookies, dict):
- self.__cookies = new_cookies
-
- @property
- def name(self):
- return self.__name
-
- @name.setter
- def name(self, new_name):
- self.__name = new_name
+ def all_headers(self) -> Dict[str, str]:
+ return self._headers
@property
- def method(self):
- return self.__method
+ def headers(self) -> Dict[str, str]:
+ if not self._class:
+ return self._headers
+ return {h: v for h, v in self._headers.items() if h not in self._class.headers}
@property
- def url(self):
- return self.__url
+ def all_cookies(self) -> Dict[str, str]:
+ return self._cookies
@property
- def body(self):
- return self.__body
+ def cookies(self) -> Dict[str, str]:
+ if not self.class_:
+ return self._cookies
+ return {c: v for c, v in self._cookies.items() if c not in self.class_.cookies}
+
+ def ensure_unique_name(self):
+ if not self._class:
+ return
+ other_methods = self._class.methods
+ if not other_methods:
+ return
+ if len(other_methods) == 1 and other_methods[0].name == self.name:
+ other_methods[0].name = f"{self.name}_one"
+ self.name = f"{self.name}_two"
+ return
+ self.name = unique_name(self.name, [method.name for method in other_methods])
+
+ def remove_headers(self):
+ self._headers = {}
+
+ def remove_cookies(self):
+ self._cookies = {}
diff --git a/autorequests/classes/outputfile.py b/autorequests/classes/outputfile.py
deleted file mode 100644
index 3de1696..0000000
--- a/autorequests/classes/outputfile.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import difflib
-from typing import Union, Optional
-
-from ..utils import PathType, cached_property
-
-
-class OutputFile:
-
- def __init__(self, filepath: Union[str, PathType], class_object):
- self.__filepath = PathType(filepath) if isinstance(filepath, str) else filepath
- self.__class = class_object
-
- def __repr__(self):
- return f""
-
- @property
- def filepath(self) -> PathType:
- return self.__filepath
-
- @property
- def class_(self):
- return self.__class
-
- @cached_property
- def text(self) -> Optional[str]:
- if self.python_file.is_file():
- return self.python_file.read_text(encoding="utf8", errors="ignore")
-
- @property
- def top(self):
- return ("import requests\n"
- "\n"
- "\n"
- "# Automatically generated by https://github.com/Hexiro/autorequests.\n"
- "\n")
-
- @cached_property
- def code(self) -> str:
- return self.top + self.class_.code
-
- @cached_property
- def folder(self) -> PathType:
- if self.filepath.name != self.class_.name:
- return self.filepath / self.class_.name
- # ex. class is named "autorequests" and output folder is named "autorequests"
- return self.filepath
-
- def in_same_dir(self) -> bool:
- return self.filepath.name == self.folder.name
-
- @property
- def python_file(self) -> PathType:
- return self.folder / "main.py"
-
- @property
- def changes_file(self) -> PathType:
- return self.folder / "changes.html"
-
- @cached_property
- def changes(self) -> str:
- return difflib.HtmlDiff().make_file(self.text.splitlines(), self.code.splitlines(), context=True)
-
- def write(self):
- self.python_file.write_text(self.code, encoding="utf8", errors="strict")
-
- def write_changes(self):
- self.changes_file.write_text(self.changes, encoding="utf8", errors="strict")
diff --git a/autorequests/classes/parameter.py b/autorequests/classes/parameter.py
index 7b70eb7..28e8017 100644
--- a/autorequests/classes/parameter.py
+++ b/autorequests/classes/parameter.py
@@ -3,10 +3,10 @@ class Parameter:
def __init__(self, name: str, **kwargs):
# isn't really used to it's full potential right now
# we'll have to see if a good way to implement custom parameters is found
- self.__name = name
+ self._name = name
# resolves to str
- self.__default = repr(kwargs["default"]) if "default" in kwargs else None
- self.__typehint = kwargs["typehint"].__name__ if "typehint" in kwargs else None
+ self._default = repr(kwargs["default"]) if "default" in kwargs else None
+ self._typehint = kwargs["typehint"].__name__ if "typehint" in kwargs else None
def __repr__(self):
return f""
@@ -23,12 +23,12 @@ def code(self):
@property
def name(self):
- return self.__name
+ return self._name
@property
def typehint(self):
- return self.__typehint
+ return self._typehint
@property
def default(self):
- return self.__default
+ return self._default
diff --git a/autorequests/classes/url.py b/autorequests/classes/url.py
index 730114b..8bf32d0 100644
--- a/autorequests/classes/url.py
+++ b/autorequests/classes/url.py
@@ -9,18 +9,18 @@ def __init__(self, url: str):
# im just gonna append it to the end of path and hope for the best
# (not the same as query string params)
# `anchor` should never matter for an api so it's not going to be supported
- self.__protocol = parsed.scheme
- self.__domain = parsed.netloc
- self.__path = parsed.path + parsed.params
+ self._protocol = parsed.scheme
+ self._domain = parsed.netloc
+ self._path = parsed.path + parsed.params
# parse to dict
query = parsed.query
- self.__query = {}
+ self._query = {}
for param in query.split("&"):
# sometimes param can be "" :shrug:
if param:
key, value = param.split("=", maxsplit=1)
- self.__query[key] = value
+ self._query[key] = value
def __repr__(self):
return f""
@@ -30,16 +30,16 @@ def __str__(self):
@property
def protocol(self) -> str:
- return self.__protocol
+ return self._protocol
@property
def domain(self) -> str:
- return self.__domain
+ return self._domain
@property
def path(self) -> str:
- return self.__path
+ return self._path
@property
def query(self) -> dict:
- return self.__query
+ return self._query
diff --git a/autorequests/utils.py b/autorequests/utilities/__init__.py
similarity index 68%
rename from autorequests/utils.py
rename to autorequests/utilities/__init__.py
index 9a2e495..06728a2 100644
--- a/autorequests/utils.py
+++ b/autorequests/utilities/__init__.py
@@ -1,22 +1,28 @@
import functools
import json
import keyword
-import string
import sys
-from pathlib import Path
-from typing import List, Dict, Iterable, Optional
-
-# Path() returns type WindowsPath or PosixPath based on os
-# I could replicate their os check, but this is safer in case they change it in the future.
-
-PathType = type(Path())
+from typing import List, Dict, Iterable, Optional, Callable, Union
+from .regexp import leading_integer_regexp
# pretty simplistic names tbf
# a lot of these aren't super self explanatory so they have docstring
+__all__ = (
+ "cached_property",
+ "indent",
+ "uses_accepted_chars",
+ "is_pythonic_name",
+ "extract_cookies",
+ "compare_dicts",
+ "format_dict",
+ "written_form",
+ "unique_name"
+)
+
-def cached_property(func):
+def cached_property(func: Callable):
if sys.version_info > (3, 8):
return functools.cached_property(func)
return property(functools.lru_cache()(func))
@@ -25,13 +31,13 @@ def cached_property(func):
def indent(data: str, spaces: int = 4) -> str:
"""
indents a code block a set amount of spaces
- note: is faster than textwrap.indent()
+ note: is ~1.5x faster than textwrap.indent(data, " " * spaces)
(from my testing)
"""
# using this var is slightly slower on small operations,
# and a lot faster on big operations
indent_block = " " * spaces
- return "\n".join(indent_block + line for line in data.splitlines())
+ return "\n".join((indent_block + line if line else line) for line in data.splitlines())
def uses_accepted_chars(text: str, chars: Iterable) -> bool:
@@ -40,18 +46,8 @@ def uses_accepted_chars(text: str, chars: Iterable) -> bool:
def is_pythonic_name(text: str) -> bool:
- if not text:
- return False
- # functions can't start with a digit
- if text[0].isdigit():
- return False
- # function names can only contain letters, numbers, and _s
- if not uses_accepted_chars(text, string.ascii_letters + string.digits + "_"):
- return False
- # if function name is a reserved keyword
- if keyword.iskeyword(text):
- return False
- return True
+ """ :returns: true if the string provided is a valid function name """
+ return text.isidentifier() and not keyword.iskeyword(text)
def extract_cookies(headers: Dict[str, str]) -> Dict[str, str]:
@@ -66,7 +62,7 @@ def extract_cookies(headers: Dict[str, str]) -> Dict[str, str]:
return cookie_dict
-def compare_dicts(dicts: List[dict]) -> dict:
+def compare_dicts(*dicts: Dict[str, str]) -> Dict[str, str]:
""" :returns: a dictionary with the items that all of the dicts in the list share """
# if there is 0 or 1 dicts, there will be no matches
if len(dicts) <= 1:
@@ -74,20 +70,7 @@ def compare_dicts(dicts: List[dict]) -> dict:
# they ALL have to share an item for it to be accepted,
# therefore we can just loop over the first dict in the list and check if it matches the other items
- return {k: v for k, v in dicts[0].items() if all(dict_.get(k) == v for dict_ in dicts[1:])}
-
-
-# compare_lists isn't used yet
-
-def compare_lists(lists: List[list]) -> list:
- """ :returns: a list of items that all the lists share """
- # if there is 0 or 1 lists, there will be no matches
- if len(lists) <= 1:
- return []
-
- # they ALL have to contain an item for it to be accepted,
- # therefore we can just loop over the first list in the list and check all the other lists share the match
- return [p for i, p in enumerate(lists[0]) if all(list_[i] == p for list_ in lists[1:])]
+ return {k: v for k, v in dicts[0].items() if all(x.get(k) == v for x in dicts[1:])}
def format_dict(data: dict, indent: Optional[int] = 4, variables: List[str] = None) -> str:
@@ -108,7 +91,7 @@ def format_dict(data: dict, indent: Optional[int] = 4, variables: List[str] = No
return formatted
-# kinda fucked if english changes
+# kinda screwed if english changes
# if english has progressed please make a pr :pray:
ones_dict = {"1": "one",
@@ -142,13 +125,32 @@ def format_dict(data: dict, indent: Optional[int] = 4, variables: List[str] = No
"19": "nineteen"}
-def written_form(num: int) -> str:
- """ :returns: written form of an integer 0-999 """
+def written_form(num: Union[int, str]) -> str:
+ """ :returns: written form of an integer 0-999, or for the leading integer of a string """
+ if isinstance(num, str):
+ # if string is an integer
+ if num.isdigit():
+ return written_form(int(num))
+ # try to parse leading integer
+ match = leading_integer_regexp.search(num)
+ if not match:
+ return num
+ # if str starts with integer
+ initial_num = match.group(0)
+ written_num = written_form(int(initial_num))
+ rest = num[match.end():]
+ return f"{written_num}_{rest}"
if num > 999:
raise NotImplementedError("numbers > 999 not supported")
+ if num < 0:
+ raise NotImplementedError("numbers < 0 not supported")
if num == 0:
return "zero"
- hundreds, tens, ones = str(num).zfill(3)
+ # mypy & pycharm don't like string unpacking
+ full_num = str(num).zfill(3)
+ hundreds = full_num[0]
+ tens = full_num[1]
+ ones = full_num[2]
ones_match = ones_dict.get(ones)
tens_match = tens_dict.get(tens)
unique_match = unique_dict.get((tens + ones))
@@ -177,15 +179,3 @@ def unique_name(name: str, other_names: List[str]) -> str:
raise NotImplementedError(">999 methods with similar names not supported")
written = written_form(matched_names_length + 1)
return name + "_" + written
-
-
-def combine_dicts(dicts: List[dict]) -> dict:
- """ combines dicts with unique names """
- combined = {}
- for dict_ in dicts:
- for key, value in dict_.items():
- if key in combined:
- key = unique_name(name=key,
- other_names=list(combined.keys()))
- combined[key] = value
- return combined
diff --git a/autorequests/utilities/case.py b/autorequests/utilities/case.py
new file mode 100644
index 0000000..f0abe33
--- /dev/null
+++ b/autorequests/utilities/case.py
@@ -0,0 +1,110 @@
+import string
+
+from . import uses_accepted_chars
+from .regexp import fix_snake_case_regexp
+
+# accepted chars for each case convention
+SNAKE_CASE_CHARS = string.ascii_lowercase + "_"
+KEBAB_CASE_CHARS = string.ascii_lowercase + "-"
+DOT_CASE_CHARS = string.ascii_lowercase + "."
+CAMEL_CASE_CHARS = string.ascii_letters
+PASCAL_CASE_CHARS = string.ascii_letters
+
+
+def snake_case(text: str):
+ """
+ Tries to parse snake case from an unknown case convention
+ """
+ if is_no_case(text):
+ return text
+ if is_kebab_case(text):
+ return kebab_to_snake(text)
+ if is_dot_case(text):
+ return dot_to_snake(text)
+ if is_camel_case(text):
+ return camel_to_snake(text)
+ if is_pascal_case(text):
+ return pascal_to_snake(text)
+ # attempt to parse text
+ snaked_text = text
+ snaked_text = kebab_to_snake(snaked_text)
+ snaked_text = dot_to_snake(snaked_text)
+ snaked_text = camel_to_snake(snaked_text)
+ # pascal to snake and camel to snake are the same function, so pascal isn't needed
+ return fix_snake_case_regexp.sub("_", snaked_text)
+
+
+def camel_case(text: str):
+ return snake_to_camel(snake_case(text))
+
+
+def pascal_case(text: str):
+ return snake_to_pascal(snake_case(text))
+
+
+def kebab_case(text: str):
+ return snake_to_kebab(snake_case(text))
+
+
+def dot_case(text: str):
+ return snake_to_dot(snake_case(text))
+
+
+def is_no_case(text: str) -> bool:
+ """
+ :returns: true if text is a single lowercase word
+ that could be considered any case convention really (besides pascal ig)
+ """
+ return text.islower() and uses_accepted_chars(text, string.ascii_lowercase)
+
+
+def is_snake_case(text: str) -> bool:
+ return text.islower() and uses_accepted_chars(text, SNAKE_CASE_CHARS)
+
+
+def is_kebab_case(text: str) -> bool:
+ return text.islower() and uses_accepted_chars(text, KEBAB_CASE_CHARS)
+
+
+def is_dot_case(text: str) -> bool:
+ return text.islower() and uses_accepted_chars(text, DOT_CASE_CHARS)
+
+
+def is_camel_case(text: str) -> bool:
+ return text[0].islower() and not text.islower() and \
+ uses_accepted_chars(text, CAMEL_CASE_CHARS)
+
+
+def is_pascal_case(text: str) -> bool:
+ return text[0].isupper() and uses_accepted_chars(text, PASCAL_CASE_CHARS)
+
+
+def camel_to_snake(text: str) -> str:
+ return "".join("_" + t.lower() if t.isupper() else t for t in text).lstrip("_")
+
+
+pascal_to_snake = camel_to_snake
+
+
+def kebab_to_snake(text: str) -> str:
+ return text.replace("-", "_")
+
+
+def dot_to_snake(text: str) -> str:
+ return text.replace(".", "_")
+
+
+def snake_to_camel(text: str) -> str:
+ return "".join(t.lower() if i == 0 else t.capitalize() for i, t in enumerate(text.split("_")))
+
+
+def snake_to_pascal(text: str) -> str:
+ return "".join(t.capitalize() for t in text.split("_"))
+
+
+def snake_to_kebab(text: str) -> str:
+ return text.replace("_", "-").lower()
+
+
+def snake_to_dot(text: str) -> str:
+ return text.replace("_", ".").lower()
diff --git a/autorequests/utilities/inspector.py b/autorequests/utilities/inspector.py
new file mode 100644
index 0000000..a3db5a2
--- /dev/null
+++ b/autorequests/utilities/inspector.py
@@ -0,0 +1,18 @@
+import inspect as _inspect
+
+from rich.syntax import Syntax
+
+from ..utilities import indent
+
+
+def inspect(cls):
+ methods = [x for x in (getattr(cls, i) for i in dir(cls)) if _inspect.isfunction(x) and x.__name__ != "__init__"]
+ signatures = []
+ for method in methods:
+ signature = f"def {method.__name__}{_inspect.signature(method)}:"
+ doc = _inspect.getdoc(method)
+ if doc:
+ # add docstring
+ signature += f"\n \"\"\"\n{indent(doc)}\n \"\"\""
+ signatures.append(signature)
+ return Syntax("\n\n".join(signatures), "python", theme="fruity")
diff --git a/autorequests/utilities/parsing.py b/autorequests/utilities/parsing.py
new file mode 100644
index 0000000..de921d7
--- /dev/null
+++ b/autorequests/utilities/parsing.py
@@ -0,0 +1,101 @@
+import json
+from typing import Optional
+
+from . import regexp
+from autorequests.classes import Method, URL, Body
+from . import extract_cookies
+
+
+def method_from_text(text: str) -> Optional[Method]: # type: ignore
+ # short circuiting
+ if text:
+ return method_from_fetch(text) or method_from_powershell(text)
+
+
+def method_from_fetch(text: str) -> Optional[Method]:
+ """
+ Parses a file that follows this format:
+ (with some being optional)
+
+ fetch(, {
+ "headers": ,
+ "referrer": ,
+ "referrerPolicy": ,
+ "body": ,
+ "method": ,
+ "mode":
+ });
+ """
+ fetch = regexp.fetch_regexp.search(text)
+ if not fetch:
+ return # type: ignore
+
+ headers = json.loads(fetch["headers"])
+ # referer is spelled wrong in the HTTP header
+ # referrer policy is not
+ referrer = fetch["referrer"]
+ referrer_policy = fetch["referrer_policy"]
+ if referrer:
+ headers["referer"] = referrer
+ if referrer_policy:
+ headers["referrer-policy"] = referrer_policy
+
+ cookies = extract_cookies(headers)
+
+ method = fetch["method"]
+ url = URL(fetch["url"])
+ body = Body(fetch["body"])
+
+ return Method(method=method,
+ url=url,
+ body=body,
+ headers=headers,
+ cookies=cookies,
+ )
+
+
+def method_from_powershell(text: str) -> Optional[Method]:
+ """
+ Parses a file that follows this format:
+ (with some potentially being optional)
+
+ Invoke-WebRequest -Uri `
+ -Method ` # optional; defaults to GET if not set
+ -Headers `
+ -ContentType ` # optional; only exists w/ body
+ -Body # optional; only exists if a body is present
+ """
+ powershell = regexp.powershell_regexp.search(text)
+ if not powershell:
+ return # type: ignore
+
+ headers = {}
+ for header in powershell["headers"].splitlines():
+ if "=" in header:
+ header = header.lstrip(" ")
+ key, value = header.split("=", maxsplit=1)
+ # remove leading and trailing "s that always exist
+ key = key[1:-1]
+ value = value[1:-1]
+ headers[key] = value
+
+ cookies = extract_cookies(headers)
+
+ # ` is the escape character in powershell
+ # replace two `s with a singular `
+ # replace singular `s with nothing
+
+ raw_body = powershell["body"]
+ if raw_body:
+ raw_body = "".join((e if e != "" else "`") for e in raw_body.split("`"))
+
+ method = powershell["method"] or "GET"
+ url = URL(powershell["url"])
+ body = Body(raw_body)
+
+ return Method(method=method,
+ url=url,
+ body=body,
+ headers=headers,
+ cookies=cookies,
+ )
diff --git a/autorequests/regexp.py b/autorequests/utilities/regexp.py
similarity index 67%
rename from autorequests/regexp.py
rename to autorequests/utilities/regexp.py
index 0a5ac6a..c203b23 100644
--- a/autorequests/regexp.py
+++ b/autorequests/utilities/regexp.py
@@ -6,11 +6,13 @@
# I keep learning that things can be optional,
# so now everything besides headers is optional just in case
fetch_regexp = re.compile(
- r"^fetch\(\"(?P(?:http|https):\/\/.+)\", {\n \"headers\": (?P{(?:.|\n)+}),\n"
- r"(?: \"referrer\": \"(?P.+)\",\n|)"
- r"(?: \"referrerPolicy\": \"(?P.+)\",\n|)"
- r"(?: \"body\": (?:\"|)(?P.+?)(?:\"|),\n|)"
- r"(?: \"method\": \"(?P[A-Z]+)\",\n|)"
+ r"^fetch\(\""
+ r"(?P(?:http|https):\/\/.+)\", {\n \"headers\": "
+ r"(?P{(?:.|\n)+}),\n(?: \"referrer\": \""
+ r"(?P.+)\",\n|)(?: \"referrerPolicy\": \""
+ r"(?P.+)\",\n|)(?: \"body\": (?:\"|)"
+ r"(?P.+?)(?:\"|),\n|)(?: \"method\": \""
+ r"(?P[A-Z]+)\",\n|)"
r"(?: \"[a-z]+\": \".+\"(?:,|)\n|)*}\);$"
)
@@ -24,3 +26,4 @@
)
fix_snake_case_regexp = re.compile("_{2,}")
+leading_integer_regexp = re.compile("^[0-9]+")
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..3ac8804
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,478 @@
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+
+[[package]]
+name = "certifi"
+version = "2021.10.8"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.6"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "dev"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "dataclasses"
+version = "0.8"
+description = "A backport of the dataclasses module for Python 3.6"
+category = "main"
+optional = false
+python-versions = ">=3.6, <3.7"
+
+[[package]]
+name = "filelock"
+version = "3.3.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
+[[package]]
+name = "idna"
+version = "3.2"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "importlib-metadata"
+version = "4.8.1"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mypy"
+version = "0.900"
+description = "Optional static typing for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3,<0.5.0"
+toml = "*"
+typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""}
+typing-extensions = ">=3.7.4"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "packaging"
+version = "21.0"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pygments"
+version = "2.10.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-mypy"
+version = "0.8.1"
+description = "Mypy static type checker plugin for Pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+attrs = ">=19.0"
+filelock = ">=3.0"
+mypy = [
+ {version = ">=0.500", markers = "python_version < \"3.8\""},
+ {version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""},
+ {version = ">=0.780", markers = "python_version >= \"3.9\""},
+]
+pytest = ">=3.5"
+
+[[package]]
+name = "requests"
+version = "2.26.0"
+description = "Python HTTP for Humans."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "rich"
+version = "10.11.0"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[package.dependencies]
+colorama = ">=0.4.0,<0.5.0"
+commonmark = ">=0.9.0,<0.10.0"
+dataclasses = {version = ">=0.7,<0.9", markers = "python_version >= \"3.6\" and python_version < \"3.7\""}
+pygments = ">=2.6.0,<3.0.0"
+typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3.8\""}
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "typed-ast"
+version = "1.4.3"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.2"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.7"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "zipp"
+version = "3.6.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.6"
+content-hash = "3bdb8018e7778edc19539fdabd7acb2f336d1aa158d2baf5cccd734410654522"
+
+[metadata.files]
+atomicwrites = [
+ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
+certifi = [
+ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
+ {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-2.0.6.tar.gz", hash = "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"},
+ {file = "charset_normalizer-2.0.6-py3-none-any.whl", hash = "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6"},
+]
+colorama = [
+ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+commonmark = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+dataclasses = [
+ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
+ {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
+]
+filelock = [
+ {file = "filelock-3.3.0-py3-none-any.whl", hash = "sha256:bbc6a0382fe8ec4744ecdf6683a2e07f65eb10ff1aff53fc02a202565446cde0"},
+ {file = "filelock-3.3.0.tar.gz", hash = "sha256:8c7eab13dc442dc249e95158bcc12dec724465919bdc9831fdbf0660f03d1785"},
+]
+idna = [
+ {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
+ {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
+]
+importlib-metadata = [
+ {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
+ {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
+]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+mypy = [
+ {file = "mypy-0.900-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:07efc88486877d595cca7f7d237e6d04d1ba6f01dc8f74a81b716270f6770968"},
+ {file = "mypy-0.900-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:80c96f97de241ee7383cfe646bfc51113a48089d50c33275af0033b98dee3b1c"},
+ {file = "mypy-0.900-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0e703c0afe36511746513d168e1d2a52f88e2a324169b87a6b6a58901c3afcf3"},
+ {file = "mypy-0.900-cp35-cp35m-win_amd64.whl", hash = "sha256:23100137579d718cd6f05d572574ca00701fa2bfc7b645ebc5130d93e2af3bee"},
+ {file = "mypy-0.900-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:468b3918b26f81d003e8e9b788c62160acb885487cf4d83a3f22ba9061cb49e2"},
+ {file = "mypy-0.900-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d90c296cd5cdef86e720a0994d41d72c06d6ff8ab8fc6aaaf0ee6c675835d596"},
+ {file = "mypy-0.900-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:42d66b3d716fe5e22b32915d1fa59e7183a0e02f00b337b834a596c1f5e37f01"},
+ {file = "mypy-0.900-cp36-cp36m-win_amd64.whl", hash = "sha256:a354613b4cc6e0e9f1ba7811083cd8f63ccee97333d1df7594c438399c83249a"},
+ {file = "mypy-0.900-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:22f97de97373dd6180c4abee90b20c60780820284d2cdc5579927c0e37854cf6"},
+ {file = "mypy-0.900-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e75f0c97cfe8d86da89b22ad7039f5af44b8f6b0af12bd2877791a92b4b9e987"},
+ {file = "mypy-0.900-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:41f082275a20e3eea48364915f7bc6ec5338be89db1ed8b2e570b9e3d12d4dc6"},
+ {file = "mypy-0.900-cp37-cp37m-win_amd64.whl", hash = "sha256:83adbf3f8c5023f4276557fbcb3b6306f9dce01783e8ac5f8c11fcb29f62e899"},
+ {file = "mypy-0.900-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2220f97804890f3e6da3f849f81f3e56e367a2027a51dde5ce3b7ebb2ad3342b"},
+ {file = "mypy-0.900-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3f1d0601842c6b4248923963fc59a6fdd05dee0fddc8b07e30c508b6a269e68f"},
+ {file = "mypy-0.900-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c2f87505840c0f3557ea4aa5893f2459daf6516adac30b15d1d5cf567e0d7939"},
+ {file = "mypy-0.900-cp38-cp38-win_amd64.whl", hash = "sha256:a0461da00ed23d17fcb04940db2b72f920435cf79be943564b717e378ffeeddf"},
+ {file = "mypy-0.900-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9560b1f572cdaab43fdcdad5ef45138e89dc191729329db1b8ce5636f4cdeacf"},
+ {file = "mypy-0.900-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d794f10b9f28d21af7a93054e217872aaf9b9ad1bd354ae5e1a3a923d734b73f"},
+ {file = "mypy-0.900-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:68fd1c1c1fc9b405f0ed6cfcd00541de7e83f41007419a125c20fa5db3881cb1"},
+ {file = "mypy-0.900-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7eb1e5820deb71e313aa2b5a5220803a9b2e3efa43475537a71d0ffed7495e1e"},
+ {file = "mypy-0.900-cp39-cp39-win_amd64.whl", hash = "sha256:6598e39cd5aa1a09d454ad39687b89cf3f3fd7cf1f9c3f81a1a2775f6f6b16f8"},
+ {file = "mypy-0.900-py3-none-any.whl", hash = "sha256:3be7c68fab8b318a2d5bcfac8e028dc77b9096ea1ec5594e9866c8fb57ae0296"},
+ {file = "mypy-0.900.tar.gz", hash = "sha256:65c78570329c54fb40f956f7645e2359af5da9d8c54baa44f461cdc7f4984108"},
+]
+mypy-extensions = [
+ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+packaging = [
+ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+py = [
+ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pygments = [
+ {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
+ {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
+]
+pyparsing = [
+ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
+pytest-mypy = [
+ {file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"},
+ {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"},
+]
+requests = [
+ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+ {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
+]
+rich = [
+ {file = "rich-10.11.0-py3-none-any.whl", hash = "sha256:44bb3f9553d00b3c8938abf89828df870322b9ba43caf3b12bb7758debdc6dec"},
+ {file = "rich-10.11.0.tar.gz", hash = "sha256:016fa105f34b69c434e7f908bb5bd7fefa9616efdb218a2917117683a6394ce5"},
+]
+toml = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+typed-ast = [
+ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
+ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
+ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
+ {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
+ {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
+ {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
+ {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
+ {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
+ {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
+ {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
+ {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
+ {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
+ {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
+ {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
+ {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
+ {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
+ {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
+ {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
+ {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
+ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
+]
+typing-extensions = [
+ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+ {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
+ {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
+]
+zipp = [
+ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
+ {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..b23fd7e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,27 @@
+[tool.poetry]
+name = "autorequests"
+version = "1.1.0"
+description = "Automatically create a simple API wrapper from request data generated by your browser."
+authors = ["Hexiro"]
+license = "MPL2"
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.6"
+rich = "^10.0.0"
+
+[tool.poetry.dev-dependencies]
+requests = "^2.26.0"
+mypy = "^0.900"
+pytest = "^6.0.0"
+pytest-mypy = "^0.8.1"
+
+[tool.poetry.scripts]
+autorequests = "autorequests:main"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.pytest.ini_options]
+addopts = "--mypy"
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index dcbc0dd..0000000
--- a/setup.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from setuptools import setup, find_packages
-
-with open("README.md", encoding="utf8") as readme_file:
- readme = readme_file.read()
-
-with open("./autorequests/__init__.py", encoding="utf8") as __init__:
- # kinda ironic how im parsing a python file in a project that parses js
- # removes leading and trailing __s from var
- # removes escape sequences and ""s from value
- INFO = {var.strip("__"): value.strip("\"").replace("\\", "") for var, value in (
- line.split(" = ") for line in __init__.read().splitlines() if line.startswith("__"))}
-
-setup(
- name="autorequests",
- version=INFO["version"],
- description=INFO["description"],
- long_description=readme,
- long_description_content_type="text/markdown",
- author="Hexiro",
- url="https://github.com/Hexiro/autorequests",
- packages=["autorequests"] + [("autorequests." + x) for x in find_packages(where="autorequests")],
- entry_points={
- "console_scripts": [
- "autorequests = autorequests.__main__:main"
- ]
- },
- python_requires=">=3.6",
- zip_safe=True,
- license="MPL2",
- classifiers=[
- "Environment :: Console",
- "Intended Audience :: Developers",
- "Natural Language :: English",
- "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python",
- "Topic :: Software Development",
- ],
- keywords=[
- "python",
- "python3",
- "node"
- ],
-)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..bf9448e
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,59 @@
+from autorequests import utilities
+
+
+# ensures utilities are working properly
+# not necessarily needed, but it can be used to help debug
+
+
+def test_indent():
+ assert utilities.indent("a") == " a"
+ assert utilities.indent("a", spaces=8) == " a"
+
+
+def test_uses_accepted_chars():
+ assert utilities.uses_accepted_chars("a", "a")
+ assert not utilities.uses_accepted_chars("a", "b")
+
+
+def test_is_pythonic_name():
+ assert utilities.is_pythonic_name("a")
+ assert not utilities.is_pythonic_name("class")
+
+
+def test_extract_cookies():
+ assert utilities.extract_cookies({}) == {}
+ headers = {"a": "a", "cookie": "a=1; b=1"}
+ assert utilities.extract_cookies(headers) == {"a": "1", "b": "1"}
+ assert "cookie" not in headers
+ assert headers.get("a") == "a"
+
+
+def test_compare_dicts():
+ dict_one = {"a": "a"}
+ dict_two = {"a": "b"}
+ assert utilities.compare_dicts(dict_one, dict_two) == {}
+ dict_one = {"a": "a"}
+ dict_two = {"a": "a"}
+ assert utilities.compare_dicts(dict_one, dict_two) == {"a": "a"}
+
+
+def test_format_dict():
+ assert utilities.format_dict({"a": "a"}) == '{\n "a": "a"\n}'
+ assert utilities.format_dict({"a": "a"}, variables=["a"]) == '{\n "a": a\n}'
+ assert utilities.format_dict({"a": False}) == '{\n "a": False\n}'
+ assert utilities.format_dict({"a": "False"}) == '{\n "a": "False"\n}'
+
+
+def test_written_form():
+ assert utilities.written_form(0) == "zero"
+ assert utilities.written_form(100) == "one_hundred"
+ assert utilities.written_form(999) == "nine_hundred_and_ninety_nine"
+ assert utilities.written_form(999) == utilities.written_form("999")
+ assert utilities.written_form("999abcdefbh") == "nine_hundred_and_ninety_nine_abcdefbh"
+ assert utilities.written_form("abcdefbh") == "abcdefbh"
+
+
+def test_unique_name():
+ assert utilities.unique_name("a", []) == "a"
+ assert utilities.unique_name("a", ["a_one"]) == "a_two"
+ assert utilities.unique_name("a", ["a_one", "a_two"]) == "a_three"