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"