├── .gitattributes
├── tracer
├── gui
│ ├── static
│ │ ├── assets
│ │ │ ├── favicon.ico
│ │ │ ├── tracer.webp
│ │ │ └── tracer_social_preview.jpg
│ │ ├── js
│ │ │ └── search.js
│ │ └── css
│ │ │ └── main.css
│ ├── __init__.py
│ ├── templates
│ │ ├── index.html
│ │ └── base.html
│ └── app.py
├── __init__.py
├── models
│ ├── __init__.py
│ ├── textanimation.py
│ ├── category.py
│ ├── result.py
│ ├── parser.py
│ ├── websitepool.py
│ └── website.py
├── __main__.py
├── loader.py
└── tracer.py
├── .editorconfig
├── data
├── logo.txt
└── pool.json
├── .github
└── ISSUE_TEMPLATE
│ ├── false-username-detection.md
│ ├── feature_request.md
│ └── bug_report.md
├── pyproject.toml
├── tests
├── test_result.py
├── test_tracer.py
├── test_website.py
└── test_websitepool.py
├── LICENSE
├── settings.conf
├── .gitignore
├── README.md
└── requirements.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/tracer/gui/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/favicon.ico
--------------------------------------------------------------------------------
/tracer/gui/static/assets/tracer.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/tracer.webp
--------------------------------------------------------------------------------
/tracer/gui/static/assets/tracer_social_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/tracer_social_preview.jpg
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | tab_width = 4
10 |
11 | [*.py]
12 | insert_final_newline = true
13 |
14 | [*.js]
15 | indent_size = 2
16 | insert_final_newline = true
17 |
--------------------------------------------------------------------------------
/tracer/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Tracer
3 | ~~~~~~
4 |
5 | A simple username checker for social media websites
6 | """
7 |
8 | __title__ = "Tracer"
9 | __author__ = "chr3st5an"
10 | __version__ = "1.0.2"
11 | __license__ = "MIT"
12 |
13 | from .models import *
14 | from .tracer import *
15 | from .loader import *
16 |
--------------------------------------------------------------------------------
/data/logo.txt:
--------------------------------------------------------------------------------
1 | A tool created by @chr3st5an
2 | ___________
3 | \__ ___/_______ _____ ____ ____ _______
4 | | | \_ __ \\__ \ _/ ___\_/ __ \\_ __ \
5 | | | | | \/ / __ \_\ \___\ ___/ | | \/
6 | |____| |__| (____ / \___ >\___ >|__|
7 | \/ \/ \/
8 | - Find them all
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/false-username-detection.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: False username detection
3 | about: Tracer doesn't detect a username on a specific website
4 | title: "[DETECTION ISSUE]"
5 | labels: false result
6 | assignees: chr3st5an
7 |
8 | ---
9 |
10 | **Which website is involved**
11 | The involved website
12 |
13 | **Did you use a VPN / Proxy**
14 | Software that obfuscated your IP address
15 |
16 | **Does the username contain non-ascii characters**
17 | Non-ascii characters are for example emojis
18 |
19 | **How long is the username**
20 | The amount of characters that the username contains
21 |
22 | **Did it work before**
23 | Did Tracer suddenly report a false result
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: chr3st5an
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "tracer"
3 | version = "1.0.2"
4 | description = "A simple username checker for social media websites"
5 | authors = ["Christian <64144555+chr3st5an@users.noreply.github.com>"]
6 | license = "MIT"
7 | readme = "README.md"
8 |
9 | [tool.poetry.dependencies]
10 | python = "^3.8.1"
11 | aiohttp = "^3.8.4"
12 | colorama = "^0.4.6"
13 | aiofiles = "^23.1.0"
14 | pyvis = "^0.3.1"
15 | aiohttp-jinja2 = "^1.5.1"
16 | Jinja2 = "^3.1.2"
17 |
18 | [tool.poetry.group.dev.dependencies]
19 | mypy = "^1.0.1"
20 | isort = "^5.12.0"
21 | flake8 = "^6.0.0"
22 |
23 | [tool.isort]
24 | combine_star = true
25 | combine_as_imports = true
26 | from_first = true
27 | lines_after_imports = 2
28 | length_sort = true
29 | line_length = 120
30 | skip_gitignore = true
31 | use_parentheses = true
32 |
33 | [build-system]
34 | requires = ["poetry-core"]
35 | build-backend = "poetry.core.masonry.api"
36 |
--------------------------------------------------------------------------------
/tests/test_result.py:
--------------------------------------------------------------------------------
1 | from tracer import Result, Website
2 | import unittest
3 |
4 |
5 | attributes = {
6 | "website": Website("localhost", "http://localhost/{}", 0),
7 | "status_code": 200,
8 | "successfully": True,
9 | "delay": 4.75,
10 | "host": "localhost",
11 | "url": "http://localhost/tracer"
12 | }
13 |
14 |
15 | class TestResult(unittest.TestCase):
16 | def testDataRepresentation(self):
17 | result = Result(**attributes)
18 |
19 | self.assertIs(result.website, attributes["website"])
20 | self.assertEqual(result.status_code, attributes["status_code"])
21 | self.assertEqual(result.successfully, attributes["successfully"])
22 | self.assertEqual(result.delay, attributes["delay"])
23 | self.assertEqual(result.host, attributes["host"])
24 | self.assertEqual(result.url, attributes["url"])
25 |
26 |
27 | if __name__ == "__main__":
28 | unittest.main()
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: chr3st5an
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/tests/test_tracer.py:
--------------------------------------------------------------------------------
1 | from tracer import Tracer
2 | import unittest
3 |
4 |
5 | data = [
6 | {
7 | "domain": "example.org",
8 | "url": "https://example.org/{}",
9 | "category": 1
10 | },
11 | {
12 | "domain": "example.com",
13 | "url": "http://example.com/user/{}",
14 | "category": 2
15 | },
16 | {
17 | "domain": "example.net",
18 | "url": "http://example.net/user/{}",
19 | "category": 2
20 | }
21 | ]
22 |
23 |
24 | class TestTracer(unittest.TestCase):
25 | def testFiltering(self):
26 | tracer = Tracer("example", data, exclude=["example.org"])
27 |
28 | self.assertEqual(len(tracer.pool), 2)
29 | self.assertEqual(tracer.pool.sites[0].domain, "example.com")
30 |
31 | tracer = Tracer("example", data, only=["example.org"])
32 |
33 | self.assertEqual(len(tracer.pool), 1)
34 | self.assertEqual(tracer.pool.sites[0].domain, "example.org")
35 |
36 |
37 | if __name__ == "__main__":
38 | unittest.main()
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 chr3st5an
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tests/test_website.py:
--------------------------------------------------------------------------------
1 | from tracer import Category, Result, Website
2 | from asyncio import iscoroutine
3 | import unittest
4 |
5 |
6 | site = Website("www.example.com", "https://example.com/{}", Category.PROGRAMMING)
7 |
8 |
9 | class TestWebsite(unittest.TestCase):
10 | def testCategory(self):
11 | self.assertEqual(site.category.as_number, Category.PROGRAMMING)
12 |
13 | def testSetUsername(self):
14 | site.set_username("idontexistandyouknowthat")
15 | self.assertEqual(site.username, "idontexistandyouknowthat", "Username didn't get set")
16 |
17 | def testURL(self):
18 | site.set_username('a')
19 | self.assertEqual(site.url, "https://example.com/a", "The username should be in the URL")
20 |
21 | def testSetResult(self):
22 | result = Result(site, 200, True, 5, "", "")
23 | site.set_result(result)
24 |
25 | self.assertIs(site.result, result, "'set_result' doesn't work")
26 |
27 | def testRequestCoro(self):
28 | self.assertTrue(iscoroutine(site.send_request(object)), "'send_request' should return a coro")
29 |
30 |
31 | if __name__ == "__main__":
32 | unittest.main()
33 |
--------------------------------------------------------------------------------
/tracer/gui/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | from .app import app
26 |
--------------------------------------------------------------------------------
/tests/test_websitepool.py:
--------------------------------------------------------------------------------
1 | from tracer import WebsitePool, Website
2 | import unittest
3 |
4 |
5 | pool = WebsitePool()
6 |
7 |
8 | class TestWebsitePool(unittest.TestCase):
9 | def testAdding(self):
10 | site = Website("www.example.com", "", 10)
11 | pool.add(site)
12 |
13 | self.assertIn(site, pool, "Added website is not in the pool")
14 |
15 | pool.remove(lambda w: w is site)
16 |
17 | self.assertNotIn(site, pool, "Removed website is still in the pool")
18 |
19 | def testIterable(self):
20 | try:
21 | for _ in pool:
22 | ...
23 | self.assertTrue(True)
24 | except:
25 | self.fail("Pool is not iterable")
26 |
27 | def testSetName(self):
28 | name = "TestName"
29 | pool.set_name(name)
30 |
31 | self.assertEqual(pool.name, name, "Setting the name for the pool didn't work")
32 |
33 | def testSetUsername(self):
34 | pool.add(Website("www.example.com", "https://example.com/{}", 0))
35 | pool.set_username('tracerino')
36 |
37 | for website in pool:
38 | self.assertEqual(website.username, 'tracerino')
39 |
40 | pool.remove(lambda website: website.username == 'tracerino')
41 |
42 |
43 | if __name__ == '__main__':
44 | unittest.main()
45 |
--------------------------------------------------------------------------------
/tracer/models/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | from .textanimation import *
26 | from .websitepool import *
27 | from .category import *
28 | from .website import *
29 | from .result import *
30 | from .parser import *
31 |
--------------------------------------------------------------------------------
/tracer/__main__.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | import sys
26 |
27 |
28 | if __name__ == "__main__":
29 | major, minor = sys.version_info[:2]
30 |
31 | if (major != 3) or not (6 < minor):
32 | print("This program requires python 3.6 < 3.x")
33 | exit(0)
34 |
35 | from tracer import Tracer
36 |
37 | Tracer.main()
38 |
--------------------------------------------------------------------------------
/settings.conf:
--------------------------------------------------------------------------------
1 | [DEFAULT]
2 | # Set a timeout for each request made
3 | #
4 | # Default: none
5 | # Values: int, none
6 | # Flags: --timeout, -t
7 | #
8 | # Example:
9 | # timeout=5
10 | timeout=none
11 |
12 | # Wether to create a result folder or not.
13 | # This also includes all files that would be
14 | # saved there
15 | #
16 | # Default: on
17 | # Values: on, off
18 | create_file_output=on
19 |
20 | # Exclude specific domain names from the sites pool
21 | #
22 | # Default: ''
23 | # Values: example.com
24 | # Flags: --exclude, -e
25 | #
26 | # Multiple sites can be passed when seperating
27 | # them with a comma
28 | #
29 | # See ./src/data/pool.json for a list of sites
30 | #
31 | # exclude=example.com, example2.com, example3.com
32 | exclude=
33 |
34 | # Exclude specific domain names from the sites pool
35 | # if they belong to the given categories
36 | #
37 | # Default: ''
38 | # Values: category
39 | # Flags: --exclude-category, -E
40 | #
41 | # Multiple categories can be passed when seperating
42 | # them with a comma
43 | #
44 | # See ./src/models/category.py for more info about categories
45 | #
46 | # exclude_category=category1, category2, category3
47 | exclude_category=
48 |
49 | # Exclude sites from the sites pool if they
50 | # do not match the given domain names
51 | #
52 | # Default: ''
53 | # Values: example.com
54 | # Flags: --only, -o
55 | #
56 | # Multiple sites can be passed when seperating
57 | # them with a comma
58 | #
59 | # See ./src/data/pool.json for a list of sites
60 | #
61 | # only=example.com, example2.com, example3.com
62 | only=
63 |
64 | # Exclude sites from the sites pool
65 | # if they do not belong to the given categories
66 | #
67 | # Default: ''
68 | # Values: category
69 | # Flags: --only-category, -O
70 | #
71 | # Multiple categories can be passed when seperating
72 | # them with a comma
73 | #
74 | # See ./src/models/category.py for more info about categories
75 | #
76 | # Example:
77 | # only_category=category1, category2, category3
78 | only_category=
79 |
80 | # Open successfull results in the default
81 | # webbrowser
82 | #
83 | # Default: off
84 | # Values: on, off
85 | # Flags: --browse, -b
86 | browse=off
87 |
88 | # Print additional information while running
89 | #
90 | # Default: off
91 | # Values: on, off
92 | # Flags: --verbose, -v
93 | verbose=off
94 |
95 | # Print all results
96 | #
97 | # Default: off
98 | # Values: on, off
99 | # Flags: --all, -a
100 | all=off
101 |
102 | # Print logo on startup
103 | #
104 | # Default: on
105 | # Values: on, off
106 | print_logo=on
107 |
108 | # Retrieve and print your IP on program startup
109 | #
110 | # Default: off
111 | # Values: on, off
112 | # Flags: --ip-check
113 | ip_check=off
114 |
115 | # Set a timeout for the IP request.
116 | # ip_check must be enabled for this to take action
117 | #
118 | # Default: none
119 | # Values: int, none
120 | #
121 | # Example:
122 | # ip_timeout=5
123 | ip_timeout=none
124 |
--------------------------------------------------------------------------------
/tracer/gui/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block scripts %}
3 |
4 | {% endblock %}
5 | {% block main %}
6 |
32 |
33 |
34 | {# Prepare result boxes #}
35 | {% for site in pool %}
36 |
45 | {% endfor %}
46 |
47 |
48 | {% endblock %}
--------------------------------------------------------------------------------
/tracer/loader.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | __all__ = ("load_logo", "load_user_agent", "load_website_data")
26 |
27 | from typing import Any, Callable, Dict, List
28 | from functools import wraps
29 | import secrets
30 | import json
31 | import os
32 |
33 | from colorama import Fore
34 |
35 |
36 | DATA_FOLDER = "../data/"
37 |
38 |
39 | def dir_switcher(__func: Callable[..., Any], /) -> Callable[..., Any]:
40 | """Helper for switching working dirs
41 |
42 | Switch the working directory to the directory
43 | that contains this file and execute the given
44 | function. After execution, switch back to the
45 | original working directory.
46 |
47 | This is necessary for relative paths to work.
48 | """
49 |
50 | @wraps(__func)
51 | def wrapper(*args: Any, **kwargs: Any) -> Any:
52 | origin = os.getcwd()
53 | os.chdir(os.path.dirname(__file__))
54 |
55 | result = __func(*args, **kwargs)
56 |
57 | return os.chdir(origin) or result
58 |
59 | return wrapper
60 |
61 |
62 | @dir_switcher
63 | def load_logo() -> str:
64 | try:
65 | with open(f"{DATA_FOLDER}logo.txt") as file:
66 | return file.read()
67 | except FileNotFoundError:
68 | return ""
69 |
70 |
71 | @dir_switcher
72 | def load_user_agent() -> str:
73 | try:
74 | with open(f"{DATA_FOLDER}user_agents.json") as file:
75 | user_agents: List[str] = json.load(file)
76 |
77 | return secrets.choice(user_agents)
78 | except FileNotFoundError:
79 | return "Tracer/1.0 (+https://github.com/chr3st5an/tracer)"
80 |
81 |
82 | @dir_switcher
83 | def load_website_data() -> Dict[str, str]:
84 | try:
85 | with open(f"{DATA_FOLDER}pool.json") as file:
86 | return json.load(file)
87 | except FileNotFoundError:
88 | print(f"{Fore.RED}[ISSUE] Couldn't find the website data "
89 | f"file!{Fore.RESET}")
90 | exit(1)
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
162 | .vscode/
--------------------------------------------------------------------------------
/tracer/gui/static/js/search.js:
--------------------------------------------------------------------------------
1 | const searchEndpoint = '/api/start_search';
2 | const domainPattern = 'https:\/\/(?:.*?\\.)*(\\w+?)\\.[a-z]{2,}\/?';
3 |
4 |
5 | window.addEventListener('load', () => {
6 | let form = document.querySelector('#search-bar form');
7 |
8 | form.addEventListener('submit', event => {
9 | let username = document.querySelector('#search-bar form input').value;
10 |
11 | // Prevent default form submission
12 | event.preventDefault();
13 |
14 | if (username.length > 2) {
15 | return startSearch(username);
16 | }
17 |
18 | document.querySelector('#search-bar form input').focus();
19 | })
20 | })
21 |
22 | /**
23 | * Invoked as soon as a search is started
24 | *
25 | * Mainly adjusts visual components and initializes
26 | * result boxes with their corresponding link
27 | */
28 | const prepareResultsPage = username => {
29 | document.title = `Tracer | Results for ${username}`;
30 |
31 | let searchBar = document.getElementById('search-bar');
32 | let input = searchBar.getElementsByTagName('input')[0];
33 | let searchButton = searchBar.getElementsByTagName('button')[0];
34 | let infoFooter = document.getElementById('info');
35 |
36 | // Move the search bar up
37 | searchBar.style.marginTop = '1%';
38 |
39 | infoFooter.style.top = 'inherit';
40 | infoFooter.style.marginTop = '30px';
41 |
42 | input.setAttribute('disabled', '');
43 | input.style.color = '#6272a4';
44 | input.style.borderBottomColor = input.style.color;
45 |
46 | searchButton.innerHTML = 'Back Home';
47 | searchButton.type = 'button';
48 |
49 | searchButton.onclick = () => {
50 | window.open('/', '_self');
51 | }
52 |
53 | // Initialize the result boxes
54 | for (let box of document.getElementsByClassName('result-box')) {
55 | let url = box.getAttribute('id').replace('{}', username);
56 | let domain = url.match(domainPattern)[1]; // Get the domain of the pre-init url
57 |
58 | box.style.display = "inherit"; // Make the box visible
59 | box.setAttribute('id', domain);
60 | box.addEventListener('click', () => {
61 | window.open(url);
62 | });
63 | }
64 | }
65 |
66 | /**
67 | * Start the search
68 | *
69 | * Send a POST request to the server with the specified
70 | * username
71 | */
72 | const startSearch = username => {
73 | let request = new XMLHttpRequest();
74 | request.open('POST', searchEndpoint, true);
75 | request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
76 |
77 | request.onreadystatechange = () => {
78 | if (request.readyState == 4 && request.status == 200) {
79 | prepareResultsPage(username);
80 | getResults();
81 | }
82 | }
83 |
84 | request.send(`username=${username}`);
85 | }
86 |
87 | /**
88 | * Retrieve the results from the server
89 | *
90 | * This is done by recursively sending GET
91 | * requests to the server
92 | */
93 | const getResults = () => {
94 | let request = new XMLHttpRequest();
95 | request.open('GET', searchEndpoint, true);
96 |
97 | request.onreadystatechange = () => {
98 | if (request.readyState == 4) {
99 | if (request.status == 200 && request.responseText !== 'FINISHED') {
100 | let response;
101 |
102 | try {
103 | response = JSON.parse(request.responseText)['result'];
104 | } catch (e) {
105 | return getResults();
106 | }
107 |
108 | let box = document.getElementById(response[1].match(domainPattern)[1]);
109 |
110 | box.style.color = (response[0] ? "#50fa7b" : "#ff5555");
111 | box.style.borderColor = box.style.color;
112 |
113 | // Avoid too many boxes on small devices by removing failed ones
114 | if (!response[0] && window.innerWidth < 992) {
115 | document.getElementById('results').removeChild(box);
116 | }
117 |
118 | return getResults();
119 | } else {
120 | return null;
121 | }
122 | }
123 | }
124 |
125 | request.send();
126 | }
127 |
--------------------------------------------------------------------------------
/tracer/gui/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 | {% block title %}Tracer | Search{% endblock %}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {%- block scripts %}{% endblock %}
38 |
39 |
40 |
41 |
42 | JavaScript is required for this site!
43 |
44 |
45 |
46 | {% block main %}{% endblock %}
47 |
48 | {% block footer %}
49 |
50 |
51 |
52 |
53 |
What is Tracer?
54 |
55 | Tracer is a username checker, which
56 | means that it allows you to check on
57 | which website a username is currently taken.
58 |
59 |
60 |
61 |
62 |
63 |
Some results are wrong!
64 |
65 | Yes, sometimes websites return unexpected
66 | responses which Tracer interprets wrong.
67 |
68 |
69 |
70 |
71 |
83 |
84 | {% endblock %}
85 |
86 |
--------------------------------------------------------------------------------
/tracer/gui/static/css/main.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap');
2 |
3 |
4 | ::placeholder {
5 | color: #f8f8f2;
6 | -webkit-touch-callout: none;
7 | -webkit-user-select: none;
8 | -khtml-user-select: none;
9 | -moz-user-select: none;
10 | -ms-user-select: none;
11 | user-select: none;
12 | }
13 |
14 | ::-ms-input-placeholder {
15 | color: #f8f8f2;
16 | -webkit-touch-callout: none;
17 | -webkit-user-select: none;
18 | -khtml-user-select: none;
19 | -moz-user-select: none;
20 | -ms-user-select: none;
21 | user-select: none;
22 | }
23 |
24 | html {
25 | min-height: 100%;
26 | }
27 |
28 | body {
29 | background-color: #282a36;
30 | overflow-x: hidden;
31 | overflow-y: auto;
32 | }
33 |
34 | button:active {
35 | transform: scale(0.8);
36 | }
37 |
38 | .noselect {
39 | user-select: none;
40 | -webkit-user-select: none;
41 | -moz-user-select: none;
42 | -ms-user-select: none;
43 | }
44 |
45 | #logo {
46 | text-align: center;
47 | }
48 |
49 | #logo img {
50 | width: 25%;
51 | height: auto;
52 | }
53 |
54 | #search-bar {
55 | margin-top: 13%;
56 | position: relative;
57 | text-align: center;
58 | transition: 1.1s all ease;
59 | }
60 |
61 | #question {
62 | color: #44475a;
63 | cursor: help;
64 | }
65 |
66 | #search-bar form {
67 | padding: 35px;
68 | }
69 |
70 | #search-bar input {
71 | background-color: transparent;
72 | border-left: none;
73 | border-top: none;
74 | border-right: none;
75 | border-bottom: 1px solid #ff79c6;
76 | color: #f8f8f2;
77 | font-family: 'Roboto Mono', monospace;
78 | font-size: 15px;
79 | padding: 10px;
80 | transition: 0.7s all ease;
81 | width: 20%;
82 | }
83 |
84 | #search-bar input:focus {
85 | outline: none;
86 | border-bottom: 1px solid #50fa7b;
87 | }
88 |
89 | #search-bar button {
90 | background-color: #ff79c6;
91 | border-radius: 25px;
92 | border-style: none;
93 | cursor: pointer;
94 | font-family: 'Roboto Mono', monospace;
95 | font-size: 15px;
96 | padding: 5px 20px;
97 | transition: 0.7s all ease;
98 | }
99 |
100 | #search-bar button:hover {
101 | background-color: #50fa7b;
102 | }
103 |
104 | #info {
105 | background-color: #44475a;
106 | color: #f8f8f2;
107 | font-family: 'Roboto Mono', monospace;
108 | left: 0;
109 | position: absolute;
110 | top: 100%;
111 | width: 100%;
112 | }
113 |
114 | #wrapper {
115 | display: grid;
116 | grid-template-columns: 1fr 1fr;
117 | margin-top: 10px;
118 | text-align: center;
119 | position: relative;
120 | }
121 |
122 | #footer {
123 | background-color: #282a36;
124 | border-top-left-radius: 35%;
125 | border-top-right-radius: 35%;
126 | bottom: 0;
127 | margin-top: 25px;
128 | text-align: center;
129 | padding: 10px;
130 | }
131 |
132 | #results {
133 | display: grid;
134 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
135 | }
136 |
137 | .result-box {
138 | display: none;
139 | border-style: dashed;
140 | border-color: #6272a4;
141 | color: #6272a4;
142 | cursor: pointer;
143 | margin: 10px;
144 | padding: 10px;
145 | text-align: center;
146 | transition: 0.7s all ease;
147 | }
148 |
149 | .result-box:hover {
150 | color: #bd93f9;
151 | border-color: #bd93f9;
152 | }
153 |
154 | .click-me {
155 | display: none;
156 | font-size: 10px;
157 | }
158 |
159 | @media only screen and (max-width: 992px) {
160 | #logo img { width: 50%; }
161 | #search-bar { margin-top: 25%; }
162 | #question { display: none; }
163 | #search-bar input { width: 90%; }
164 | #search-bar button { margin-top: 25px; }
165 | #info { display: none; }
166 | #results { grid-template-columns: 1fr 1fr 1fr 1fr; }
167 | .click-me { display: inherit; }
168 | }
169 |
170 | @media only screen and (max-width: 768px) {
171 | #search-bar { margin-top: 35%; }
172 | #logo img { width: 65%; }
173 | #results { grid-template-columns: 1fr 1fr 1fr; }
174 | }
175 |
176 | @media only screen and (max-width: 576px) {
177 | #search-bar { margin-top: 45%; }
178 | #search-bar input { width: 90%; }
179 | #logo img { width: 80%; }
180 | #results { grid-template-columns: 1fr 1fr; }
181 | .result-box { padding: 5px; margin: 5px; }
182 | }
183 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://github.com/chr3st5an)
4 | [](https://www.python.org/downloads/)
5 | [](https://en.wikipedia.org/wiki/Open-source_intelligence)
6 | [](https://github.com/chr3st5an/tracer/blob/main/LICENSE)
7 | 
8 | [](https://github.com/chr3st5an/tracer)
9 |
10 | ### Tracer
11 |
12 | Tracer detects on which website a username is currently in use!
13 |
14 | **[Official Repository](https://github.com/chr3st5an/tracer)** · **[Report Bug](https://github.com/chr3st5an/tracer/issues)**
15 |
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | ---
23 |
24 | Tracer provides the following features:
25 |
26 | - 170+ sites that are checked
27 |
28 | - Filter websites based on their domain or category
29 |
30 | - Limit the pool of sites that will be checked
31 |
32 | - Browser version (GUI)
33 |
34 | - Save the result of each check in a report file
35 |
36 | - Open successful results in your browser
37 |
38 | - Customizability:
39 |
40 | - Use the included config file to change the behavior of Tracer
41 |
42 | - Easy to use
43 |
44 |
45 |
46 | [(Beam me up)](#tracer)
47 |
48 |
49 |
50 |
51 |
52 | ## Built With
53 |
54 | ---
55 |
56 | - 
57 |
58 | - 
59 |
60 |
61 |
62 | ## Getting Started
63 |
64 | ---
65 |
66 | ### Prerequisites
67 |
68 | For Tracer to work, you will need Python 3.7 or later and pip. You can download Python from the [official website](https://www.python.org/downloads/). Python ships with pip. Verify the versions:
69 |
70 | - python
71 |
72 | ```bash
73 | python -V
74 | ```
75 |
76 | - pip
77 |
78 | ```bash
79 | pip -V # or "python -m pip -V"
80 | ```
81 |
82 |
83 |
84 | [(Beam me up)](#tracer)
85 |
86 |
87 |
88 | ### Installation
89 |
90 | 1. Clone this repository
91 |
92 | ```bash
93 | git clone https://github.com/chr3st5an/tracer.git
94 | ```
95 |
96 | > 🛈 If you do not have `git`, you can download this repository by clicking on `Code` > `Download ZIP`. Unzip the folder and open a terminal.
97 |
98 | 2. Navigate into the just downloaded folder
99 |
100 | ```bash
101 | cd tracer/
102 | ```
103 |
104 | 3. Install dependencies
105 |
106 | ```bash
107 | pip install -r ./requirements.txt
108 | ```
109 |
110 |
111 |
112 | [(Beam me up)](#tracer)
113 |
114 |
115 |
116 |
117 |
118 | ## Usage
119 |
120 | ---
121 |
122 | After you installed all dependencies you are ready to run Tracer for the first time 🎉 To do so, open a terminal in the project's root folder and run the following command:
123 |
124 | ```bash
125 | python tracer [OPTIONS] username
126 | ```
127 |
128 | Where `[OPTIONS]` are optional flags you can pass to Tracer to modify its behavior. More about options [later](#options).
129 |
130 |
131 |
132 | [(Beam me up)](#tracer)
133 |
134 |
135 |
136 |
137 |
138 | ## GUI
139 |
140 | ---
141 |
142 | Tracer also offers a GUI in form of a webapp. You can run the webapp by executing the following command:
143 |
144 | ```bash
145 | python tracer --web tracer
146 | ```
147 |
148 | This will run the webapp on port 12345. Tracer should automatically open your browser and connect to the webapp. If not, open your browser manually and type `http://127.0.0.1:12345` into the search bar and hit enter.
149 |
150 | 
151 |
152 |
153 |
154 | [(Beam me up)](#tracer)
155 |
156 |
157 |
158 |
159 |
160 | ## Options
161 |
162 | ---
163 |
164 | For a list of all available commands and options, use the `-h` flag or read the following section
165 |
166 | ```bash
167 | python tracer -h
168 | ```
169 |
170 |
171 |
172 | Options
173 |
174 | - `-h`, `--help` *print a help message and exit*
175 |
176 | - `-t ` *set a timeout for requests*
177 |
178 | - `-e ` *exclude a domain*
179 |
180 | - `-o ` *only check this domain for the username*
181 |
182 | - `-O ` *only check sites that fall under this category for the username*
183 |
184 | - `-E ` *exclude all sites that fall under this category*
185 |
186 | - `-b` *open sites on which the username got found, in your default browser*
187 |
188 | - `-v` *print additional information while the program runs*
189 |
190 | - `-a` *print all websites*
191 |
192 | - `--web` *run a GUI in form of a local webapp*
193 |
194 | - `--ip-check` *retrieve your public IP address before starting the main program*
195 |
196 |
197 |
198 |
199 |
200 | [(Beam me up)](#tracer)
201 |
202 |
203 |
204 |
205 |
206 | ## License
207 |
208 | ---
209 |
210 | This project is licensed under the **MIT** license. For more information check out the project's license file.
211 |
212 |
213 |
214 | [(Beam me up)](#tracer)
215 |
216 |
217 |
218 |
219 |
220 |
225 |
--------------------------------------------------------------------------------
/tracer/gui/app.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | import secrets
26 | import asyncio
27 | import os
28 |
29 | from aiohttp import ClientSession, DummyCookieJar, web
30 | from aiohttp.web import Request, RouteTableDef
31 | from jinja2 import FileSystemLoader
32 | import aiohttp_jinja2
33 |
34 | from tracer import (
35 | load_website_data,
36 | load_user_agent,
37 | WebsitePool,
38 | Website,
39 | )
40 |
41 |
42 | os.chdir(os.path.realpath(os.path.dirname(__file__)))
43 |
44 | app = web.Application()
45 | cookies = {}
46 | routes = RouteTableDef()
47 |
48 | aiohttp_jinja2.setup(
49 | app=app,
50 | enable_async=True,
51 | loader=FileSystemLoader("./templates")
52 | )
53 |
54 |
55 | @routes.get("/")
56 | async def index(request: Request):
57 | """Represents the index page
58 | """
59 |
60 | return await aiohttp_jinja2.render_template_async(
61 | template_name="index.html",
62 | request=request,
63 | context={
64 | "host": request.host,
65 | "scheme": request.scheme,
66 | "pool": load_website_data()
67 | }
68 | )
69 |
70 |
71 | @routes.post("/api/start_search")
72 | async def start_search(request: Request):
73 | """Endpoint for starting a username search
74 |
75 | Validate data and return a searchID in form of a cookie.
76 | The client can now send GET requests to this endpoint
77 | in order to retrieve the results of the search.
78 | """
79 |
80 | username = (await request.post()).get("username", "")
81 | search_id = secrets.token_urlsafe(20)
82 | queue = asyncio.Queue()
83 |
84 | response = web.Response()
85 | response.set_cookie("search_id", search_id, max_age=300)
86 |
87 | cookies[search_id] = [queue, False]
88 |
89 | # Spawn background task
90 | asyncio.create_task(
91 | start_requests(username, queue, cookies[search_id])
92 | )
93 |
94 | return response
95 |
96 |
97 | @routes.get("/api/start_search")
98 | async def get_results(request: Request):
99 | """Endpoint for getting the results of a search
100 |
101 | Uses the searchID to identify the client and their
102 | results.
103 | """
104 |
105 | search_id = request.cookies.get("search_id", "")
106 |
107 | if search_id not in cookies:
108 | return web.Response(status=400, text="Bad Request")
109 |
110 | while True:
111 | try:
112 | result = await asyncio.wait_for(cookies[search_id][0].get(), 1)
113 |
114 | return web.json_response({"result": result})
115 | except asyncio.TimeoutError:
116 | if cookies[search_id][0].empty() and cookies[search_id][1]:
117 | del cookies[search_id]
118 |
119 | response = web.Response(text="Finished")
120 | response.del_cookie("search_id")
121 |
122 | return response
123 |
124 |
125 | async def start_requests(
126 | username: str,
127 | queue: asyncio.Queue,
128 | cookie: list
129 | ) -> None:
130 | """Send the necessary requests
131 |
132 | Put the incoming responses into the given queue. This
133 | coro is intended to be spawned as a background task.
134 |
135 | Parameters
136 | ----------
137 | username : str
138 | Check if this username is taken
139 | queue : asyncio.Queue
140 | A queue to put the results into. Results are
141 | given as a list which follow this structure:
142 | `[username_exists : bool, url : str, delay : float]`
143 | cookie : list
144 | Cookie data assigned by `start_search` where the
145 | second element represents the state of this coro.
146 | As soon as this coro finishes, the coro changes
147 | the second element to `True`
148 | """
149 |
150 | headers = {"User-Agent": load_user_agent()}
151 | cookie_jar = DummyCookieJar()
152 |
153 | async with ClientSession(headers=headers, cookie_jar=cookie_jar) as session:
154 | pool = WebsitePool(*[
155 | Website.from_dict(data) for data in load_website_data()
156 | ])
157 | pool.set_username(username)
158 |
159 | requests = pool.start_requests(session)
160 |
161 | async for response in requests:
162 | await queue.put([
163 | response.successfully,
164 | response.url,
165 | response.ms
166 | ])
167 |
168 | # Finished
169 | cookie[1] = True
170 |
171 |
172 | @routes.get("/favicon.ico")
173 | async def icon(request: Request) -> web.FileResponse:
174 | return web.FileResponse(path="./static/assets/favicon.ico")
175 |
176 |
177 | routes.static("/static", "./static")
178 | app.router.add_routes(routes)
179 |
--------------------------------------------------------------------------------
/tracer/models/textanimation.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | __all__ = ("AsyncTextAnimation",)
26 |
27 | from typing import Any, Callable, Generator, Tuple, Optional
28 | from abc import ABC, abstractmethod
29 | import asyncio
30 |
31 | from colorama import Fore
32 |
33 |
34 | class AbstractTextAnimation(ABC):
35 | @abstractmethod
36 | def set_condition(self, callable):
37 | pass
38 |
39 | @abstractmethod
40 | def set_message(self, message):
41 | pass
42 |
43 | @abstractmethod
44 | async def start(self):
45 | pass
46 |
47 |
48 | class AsyncTextAnimation(AbstractTextAnimation):
49 | """Represents a simple text animation
50 |
51 | Attributes
52 | ----------
53 | message : str
54 | The message that gets displayed
55 | condition : Callable[..., bool]
56 | A callable object representing the loop condition. As
57 | soon as it returns `False`, the animation will stop
58 | args : Tuple[Any]
59 | Args that get passed to the condition when calling it
60 | colored : bool
61 | If the animation should be colored
62 |
63 | Supported Operations
64 | --------------------
65 | `str(obj)`
66 | Returns the str representation of the text animation
67 | `len(obj)`
68 | Returns the length of the message
69 |
70 | Note
71 | ----
72 | While the animation is being displayed, there shouldn't
73 | be any other part in the program writing to the stdout
74 |
75 | Author
76 | ------
77 | chr3st5an
78 | """
79 |
80 | def __init__(
81 | self,
82 | message: str,
83 | condition: Callable[..., bool],
84 | *args,
85 | color: bool = True
86 | ):
87 | """Create an animation object
88 |
89 | Parameters
90 | ----------
91 | message : str
92 | The message that gets displayed
93 | condition : Callable[..., bool]
94 | A callable object representing the loop condition. As
95 | soon as it returns `False`, the animation will stop
96 | *args : Any
97 | Args that get passed to the condition when calling it
98 | color : bool, optional
99 | If the animation should be colored, by default True
100 | """
101 |
102 | self.set_message(message)
103 | self.set_condition(condition)
104 |
105 | self.__args = args
106 | self.__color = bool(color)
107 | self.__message = None
108 | self.__condition = None
109 |
110 | def __str__(self) -> str:
111 | return (f"<{self.__class__.__qualname__}(message={self.__message!r}, "
112 | f"condition={self.__condition}, args={self.__args}, "
113 | f"colored={self.__color})>")
114 |
115 | def __len__(self) -> int:
116 | return len(self.__message)
117 |
118 | def __await__(self) -> Generator:
119 | spinner_sequence = "|/-\\"
120 |
121 | while self.__condition(*self.__args):
122 | for char in spinner_sequence:
123 | spinner = f"[{Fore.CYAN}{char}{Fore.RESET}]" if self.colored else f"[{char}]"
124 |
125 | print(f"\r{spinner} {self.__message}", end="", flush=True)
126 |
127 | yield from asyncio.sleep(0.1).__await__()
128 |
129 | # Removes the loading message
130 | print("\r" + (" "*(len(self) + 10)), end="\r")
131 |
132 | @property
133 | def message(self) -> Optional[str]:
134 | return self.__message
135 |
136 | @property
137 | def condition(self) -> Optional[Callable[..., bool]]:
138 | return self.__condition
139 |
140 | @property
141 | def args(self) -> Tuple[Any]:
142 | return self.__args
143 |
144 | @property
145 | def colored(self) -> bool:
146 | return self.__color
147 |
148 | def set_message(self, message: str) -> None:
149 | """Assigns a new message to the animation
150 |
151 | Parameters
152 | ----------
153 | message : str
154 | The message to set for the animation object
155 | """
156 |
157 | self.__message = message
158 |
159 | def set_condition(self, condition: Callable[..., bool]) -> None:
160 | """Sets a new condition
161 |
162 | Parameters
163 | ----------
164 | condition : Callable[..., bool]
165 | A callable object representing the loop condition. As
166 | soon as it returns `False`, the animation will stop
167 | """
168 |
169 | self.__condition = condition
170 |
171 | async def start(self) -> None:
172 | """Displays a loading animation as long as the condition is True
173 |
174 | This is done by creating and awaiting an `asyncio.Task` object.
175 | While the animation is displayed, there shouldn't be any other
176 | function using `print`.
177 | """
178 |
179 | await self
180 |
--------------------------------------------------------------------------------
/tracer/models/category.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | from __future__ import annotations
26 |
27 | __all__ = ("Category",)
28 |
29 | from abc import ABC, abstractmethod
30 | from typing import Any, Dict, List
31 | from copy import deepcopy
32 |
33 |
34 | class AbstractCategory(ABC):
35 | @property
36 | @abstractmethod
37 | def website(self):
38 | pass
39 |
40 |
41 | class Category(AbstractCategory):
42 | """Used to represent the category of a site
43 |
44 | Attributes
45 | ----------
46 | website : tracer.Website
47 | The website to which the category belongs to
48 | as_number : int
49 | The int representation of the category
50 | as_str : str
51 | A str containing the category name
52 |
53 | Classmethods
54 | ------------
55 | cls.to_number(str) -> int
56 | Converts the str name of a category into the
57 | corresponding int value
58 | cls.to_str(int) -> str
59 | Converts the int representation of a category
60 | into the str name of the category
61 | cls.all_categories() -> List[str]
62 | Returns a list with all available categories.
63 |
64 | Supported Operations
65 | --------------------
66 | `str(obj)`
67 | Returns the str representation of the category
68 | `int(obj)`
69 | Alias for `obj.as_number`
70 | `x == obj`
71 | Compares the int values of both objects
72 | `copy.copy(obj)`
73 | Returns a copy of the category
74 | `copy.deepcopy(obj)`
75 | Returns a deepcopy of the category
76 |
77 | Author
78 | ------
79 | chr3st5an
80 | """
81 |
82 | SOCIALMEDIA = 1
83 | XXX = 2
84 | BLOG = 3
85 | ART = 4
86 | PROGRAMMING = 5
87 | VIDEO = 6
88 | MESSAGING = 7
89 | DATING = 8
90 | MUSIC = 9
91 | SPORT = 10
92 | MEMES = 11
93 | OFFICE = 12
94 | NEWS = 13
95 | GAMES = 14
96 | LINKS = 15
97 | OTHER = 16
98 |
99 | @classmethod
100 | def to_number(cls, category: str) -> int:
101 | """Returns the int representation of the given category
102 |
103 | Parameters
104 | ----------
105 | category : str
106 | The category
107 |
108 | Returns
109 | -------
110 | int
111 | The int representation of the category. -1 if the
112 | given category doesn't exist
113 | """
114 |
115 | return cls.__dict__.get(category.upper(), -1)
116 |
117 | @classmethod
118 | def to_str(cls, number: int) -> str:
119 | """Returns the str representation of the given category
120 |
121 | Parameters
122 | ----------
123 | number : int
124 | Any number
125 |
126 | Returns
127 | -------
128 | str
129 | The corresponding str representation of the number.
130 | "UNKNOWN" if there is no corresponding
131 | representation.
132 | """
133 |
134 | try:
135 | return [
136 | key for key, val in cls.__dict__.items() if val == number
137 | ][0]
138 | except IndexError:
139 | return "UNKNOWN"
140 |
141 | @classmethod
142 | def all_categories(cls) -> List[str]:
143 | """Returns a list with all categories
144 |
145 | Returns
146 | -------
147 | List[str]
148 | A list containing every category that is part
149 | of this class
150 | """
151 |
152 | return [attr for attr in dir(cls) if attr.isupper()]
153 |
154 | def __init__(
155 | self,
156 | website,
157 | category_number: int
158 | ):
159 | """Initializes a category
160 |
161 | Parameters
162 | ----------
163 | website : tracer.Website
164 | The website that belongs to this category
165 | category_number : int
166 | The number of the category to represent with
167 | this object
168 |
169 | Example
170 | -------
171 | >>> my_category = Category(..., Category.VIDEO)
172 |
173 | >>> print(my_category) #
174 |
175 | >>> print(my_category == Category.VIDEO) # True
176 | """
177 |
178 | self.__website = website
179 | self.__number = category_number
180 |
181 | def __str__(self) -> str:
182 | return f"<{self.__class__.__qualname__}[{self.as_str}]>"
183 |
184 | def __int__(self) -> int:
185 | return self.__number
186 |
187 | def __eq__(self, other: Any) -> bool:
188 | try:
189 | return int(self) == int(other)
190 | except Exception:
191 | return False
192 |
193 | def __copy__(self) -> Category:
194 | category = self.__class__.__new__(self.__class__)
195 | category.__dict__.update(self.__dict__)
196 |
197 | return category
198 |
199 | def __deepcopy__(self, memo: Dict[int, Any]) -> Category:
200 | category = self.__class__.__new__(self.__class__)
201 |
202 | memo[id(self)] = category
203 |
204 | for k, v in self.__dict__.items():
205 | setattr(category, k, deepcopy(v, memo))
206 |
207 | return category
208 |
209 | @property
210 | def website(self):
211 | return self.__website
212 |
213 | @property
214 | def as_number(self) -> int:
215 | return self.__number
216 |
217 | @property
218 | def as_str(self) -> str:
219 | return self.to_str(self.__number).lower()
220 |
--------------------------------------------------------------------------------
/tracer/models/result.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | __all__ = ("Result",)
26 |
27 | from typing import Optional
28 | from colorama import Fore
29 |
30 |
31 | class Result(object):
32 | """Represents the result of a HTTP request
33 |
34 | Objects of this class are mainly intended for
35 | view-only use and hence don't offer big
36 | functionalities. Objects of this class are
37 | in a close relationship with `tracer.Website`
38 | instances as the main aim of this class is
39 | to represent the results of the requests performed
40 | by those instances.
41 |
42 | Attributes
43 | ----------
44 | website : tracer.Website
45 | The website that is associated with the result.
46 | status_code : int
47 | The returned status code of the HTTP request
48 | successfully : bool
49 | If the request was successfully in the sense of
50 | that the username exists.
51 | user_exists : bool
52 | Alias for `obj.successfully`.
53 | delay : float
54 | The amount of seconds it took for a response.
55 | ms : float
56 | Alias for `obj.delay`.
57 | host : str
58 | The domain name of the website.
59 | url : str
60 | The request URL.
61 | timeout : bool
62 | If a TimeoutError occurred while performing
63 | the request.
64 | error : Exception, optional
65 | Any other excpetion that might have occurred while
66 | performing the request. None if none occurred.
67 |
68 | Methods
69 | -------
70 | obj.verbose(Optional[bool]) -> str
71 | Returns a string containing the key information of the
72 | result.
73 |
74 | Supported Operations
75 | --------------------
76 | `str(obj)`
77 | Returns the str representation of the result
78 | `bool(obj)`
79 | Alias for `obj.successfully`
80 |
81 | Author
82 | ------
83 | chr3st5an
84 | """
85 |
86 | __slots__ = (
87 | "__website",
88 | "__status_code",
89 | "__successfully",
90 | "__delay",
91 | "__host",
92 | "__url",
93 | "__timeout",
94 | "__error",
95 | )
96 |
97 | def __init__(
98 | self,
99 | website,
100 | status_code: int,
101 | successfully: bool,
102 | delay: float,
103 | host: str,
104 | url: str,
105 | timeout: bool = False,
106 | error: Optional[Exception] = None
107 | ):
108 | """Represents the result of a request
109 |
110 | Parameters
111 | ----------
112 | website : tracer.Website
113 | The website to which the result belongs to
114 | status_code : int
115 | The returned status code of the request
116 | successfully : bool
117 | If the request was successfully, alias if the
118 | username exists
119 | delay : float
120 | How many seconds it took for a response
121 | host : str
122 | The host of the website to which a requests got
123 | send (alias the domain)
124 | url : str
125 | The request url
126 | timeout : bool, optional
127 | If the request timed out, by default False
128 | error : Exception, optional
129 | Any exception that might have occurred, by default None
130 | """
131 |
132 | self.__website = website
133 | self.__status_code = status_code
134 | self.__successfully = successfully
135 | self.__delay = round(delay, 3)
136 | self.__host = host
137 | self.__url = url
138 | self.__timeout = timeout
139 | self.__error = error
140 |
141 | def __str__(self) -> str:
142 | return (f"<{self.__class__.__qualname__}("
143 | f"user_exists={self.user_exists}, delay={self.delay}, "
144 | f"timeout={self.timeout}, error={bool(self.error)})>")
145 |
146 | def __bool__(self) -> bool:
147 | return self.__successfully
148 |
149 | @property
150 | def website(self):
151 | return self.__website
152 |
153 | @property
154 | def status_code(self) -> int:
155 | return self.__status_code
156 |
157 | @property
158 | def successfully(self) -> bool:
159 | return self.__successfully
160 |
161 | @property
162 | def user_exists(self) -> bool:
163 | """Alias for `result.successfully`"""
164 |
165 | return self.successfully
166 |
167 | @property
168 | def delay(self) -> float:
169 | return self.__delay
170 |
171 | @property
172 | def ms(self) -> float:
173 | """Alias for `result.delay`"""
174 |
175 | return self.delay
176 |
177 | @property
178 | def host(self) -> str:
179 | return self.__host
180 |
181 | @property
182 | def url(self) -> str:
183 | return self.__url
184 |
185 | @property
186 | def timeout(self) -> bool:
187 | return self.__timeout
188 |
189 | @property
190 | def error(self) -> Optional[Exception]:
191 | return self.__error
192 |
193 | def verbose(self, colored: bool = True) -> str:
194 | """Creates a verbose string
195 |
196 | The string returned by this method is mainly
197 | intended for logging purposes. It contains
198 | the delay, host and status code of the request.
199 |
200 | Parameters
201 | ----------
202 | colored : bool, optional
203 | If the string should be colored, by default True
204 |
205 | Returns
206 | -------
207 | str
208 | Verbose string
209 | """
210 |
211 | message = f"{self.ms}s <=> {self.host} <=> {self.status_code}"
212 |
213 | return f"{Fore.CYAN}{message}{Fore.RESET}" if colored else message
214 |
--------------------------------------------------------------------------------
/tracer/models/parser.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | __all__ = ("TracerParser",)
26 |
27 | from configparser import ConfigParser
28 | from typing import Any, Dict, Union
29 | from abc import ABC, abstractmethod
30 | from argparse import ArgumentParser
31 | from pathlib import Path
32 | import os
33 | import re
34 |
35 | from .category import Category
36 |
37 |
38 | class AbstractTracerParser(ABC):
39 | @abstractmethod
40 | def parse(self):
41 | pass
42 |
43 |
44 | class TracerParser(AbstractTracerParser):
45 | """Parses relevant args for Tracer
46 |
47 | Attributes
48 | ----------
49 | config_file : Union[Path, str]
50 | Path to a `.conf` file
51 |
52 | Methods
53 | -------
54 | obj.parse() -> Dict
55 | Parses args from the config file and from the
56 | CLI.
57 |
58 | Supported Operations
59 | --------------------
60 | `str(obj)`
61 | Returns the str representation of the parser
62 |
63 | Note
64 | ----
65 | The given config file has to include a `[DEFAULT]`
66 | header as the parser looks for values defined under
67 | that header.
68 |
69 | Author
70 | ------
71 | chr3st5an
72 | """
73 |
74 | __slots__ = ("config_file",)
75 |
76 | def __init__(self, config_file: Union[Path, str]):
77 | """Creates a parser
78 |
79 | Parameters
80 | ----------
81 | config_file : Union[Path, str]
82 | Path to a `.conf` file
83 |
84 | Note
85 | ----
86 | The config file has to include a `[DEFAULT]`
87 | header
88 | """
89 |
90 | self.config_file = config_file
91 |
92 | def __str__(self) -> str:
93 | return f"<{self.__class__.__qualname__}(config_file={self.config_file})>"
94 |
95 | def parse(self) -> Dict[Any, Any]:
96 | """Parses args from the config file and the CLI
97 |
98 | Returns
99 | -------
100 | Dict[str, str]
101 | A dict containing the given args
102 | """
103 |
104 | kwargs = self._parse_conf_file()
105 | kwargs.update(self._parse_console())
106 |
107 | return kwargs
108 |
109 | def _parse_conf_file(self) -> Dict[str, Any]:
110 | """Parses args from the config file
111 |
112 | Returns
113 | -------
114 | Dict[str, str]
115 | A dict containing the given args
116 | """
117 |
118 | if not os.path.exists(self.config_file):
119 | return dict()
120 |
121 | parser = ConfigParser()
122 | parser.read(self.config_file)
123 |
124 | settings = dict(parser["DEFAULT"])
125 |
126 | # Converts the options from the conf file into
127 | # corresponding datatypes
128 | for setting, value in settings.items():
129 | if value.lower() == "off":
130 | settings[setting] = False
131 | elif value.lower() == "on":
132 | settings[setting] = True
133 | elif value.lower() == "none":
134 | settings[setting] = None
135 | elif value == "":
136 | settings[setting] = list()
137 | elif value.isdigit():
138 | settings[setting] = float(value)
139 | else:
140 | settings[setting] = re.findall(r"[a-zA-Z0-9.]+", value)
141 |
142 | return settings
143 |
144 | def _parse_console(self) -> Dict[str, Any]:
145 | """Parses provided options from the CLI
146 |
147 | Returns
148 | -------
149 | Dict[str, Any]
150 | A dict containing the provided options
151 | """
152 |
153 | parser = ArgumentParser(
154 | prog="tracer",
155 | usage="%(prog)s [options] username",
156 | description=("Check on which website the specified "
157 | "username is in use"),
158 | epilog="A tool created by @chr3st5an",
159 | )
160 |
161 | parser.add_argument(
162 | "username",
163 | metavar="username",
164 | type=str,
165 | help="The username to check"
166 | )
167 | parser.add_argument(
168 | "-t",
169 | "--timeout",
170 | type=int,
171 | help="set a timeout for each request",
172 | )
173 | parser.add_argument(
174 | "-e",
175 | "--exclude",
176 | type=str,
177 | default=list(),
178 | action="append",
179 | help=("exclude a website, e.g. instagram.com. "
180 | "Can be used multiple times"),
181 | )
182 | parser.add_argument(
183 | "-o",
184 | "--only",
185 | type=str,
186 | default=list(),
187 | action="append",
188 | help=("sent a request only to the given site. Can "
189 | "be used multiple times"),
190 | )
191 | parser.add_argument(
192 | "-E",
193 | "--exclude-category",
194 | type=str,
195 | default=list(),
196 | action="append",
197 | help=("exclude every website which belongs to the given category. "
198 | f"Categories: {', '.join(Category.all_categories())}"),
199 | )
200 | parser.add_argument(
201 | "-O",
202 | "--only-category",
203 | type=str,
204 | default=list(),
205 | action="append",
206 | help=("sent requests only to the sites belonging "
207 | "to the given category. See a list of all "
208 | "categories under -E"),
209 | )
210 | parser.add_argument(
211 | "-b",
212 | "--browse",
213 | default=False,
214 | action="store_true",
215 | help="open successful results in browser",
216 | )
217 | parser.add_argument(
218 | "-v",
219 | "--verbose",
220 | default=False,
221 | action="store_true",
222 | help="show additional information",
223 | )
224 | parser.add_argument(
225 | "-a",
226 | "--all",
227 | default=False,
228 | action="store_true",
229 | help="show all results"
230 | )
231 | parser.add_argument(
232 | "--ip-check",
233 | default=False,
234 | action="store_true",
235 | help=("retrieve and print your IP on program "
236 | "startup and wait 3s before continuing"),
237 | )
238 | parser.add_argument(
239 | "--web",
240 | default=False,
241 | action="store_true",
242 | help="start a GUI version of Tracer in your browser"
243 | )
244 |
245 | args = {}
246 |
247 | # Parses the args and filters options out that didn't were provided
248 | for key, value in dict(parser.parse_args()._get_kwargs()).items():
249 | if not (value == parser.get_default(key)):
250 | args[key] = value
251 |
252 | return args
253 |
--------------------------------------------------------------------------------
/tracer/models/websitepool.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | from __future__ import annotations
26 |
27 | __all__ = ("WebsitePool",)
28 |
29 | from abc import ABC, abstractmethod
30 | from typing import (
31 | Any,
32 | AsyncGenerator,
33 | Callable,
34 | Dict,
35 | Generator,
36 | Tuple,
37 | Optional
38 | )
39 | import asyncio
40 | import copy
41 |
42 | from aiohttp import ClientSession
43 |
44 | from .website import Website
45 | from .result import Result
46 |
47 |
48 | class AbstractWebsitePool(ABC):
49 | @property
50 | @abstractmethod
51 | def sites(self):
52 | pass
53 |
54 | @abstractmethod
55 | def set_name(self, name):
56 | pass
57 |
58 | @abstractmethod
59 | def set_username(self, username):
60 | pass
61 |
62 | @abstractmethod
63 | async def start_requests(self, session, *args):
64 | pass
65 |
66 |
67 | class WebsitePool(AbstractWebsitePool):
68 | """Represents a collection of `tracer.Website` instances
69 |
70 | Attributes
71 | ----------
72 | sites : Tuple[tracer.Website]
73 | All websites that are currently in the pool
74 | name : Optional[str]
75 | The name of the pool
76 | results : Tuple[tracer.Result]
77 | A collection of available results
78 |
79 | Methods
80 | -------
81 | obj.set_name(str)
82 | Sets the name of the pool
83 | obj.set_username(str)
84 | Sets the username for every website inside the pool
85 | obj.add(tracer.Website)
86 | Adds a website to the pool
87 | obj.remove(Callable[[tracer.Website], bool])
88 | Removes websites from the pool
89 | obj.start_requests(aiohttp.ClientSession, Optional[float])
90 | Calls `send_request` of every site inside of the pool
91 |
92 | Supported Operations
93 | --------------------
94 | `str(obj)`
95 | Returns the str representation of the pool
96 | `len(obj)`
97 | Returns the amount of websites that are currently in
98 | the pool
99 | `iter(obj)`
100 | Returns a generator containing all websites that are
101 | currently in the pool
102 | `x in obj`
103 | Checks if a website is currently in the pool
104 | `copy.copy(obj)`
105 | Returns a copy of the pool
106 | `copy.deepcopy(obj)`
107 | Returns a deepcopy of the pool
108 |
109 | Author
110 | ------
111 | chr3st5an
112 | """
113 |
114 | def __init__(
115 | self,
116 | *sites: Website,
117 | name: Optional[str] = None,
118 | allow_duplicates: bool = False
119 | ):
120 | """Initialize a WebsitePool
121 |
122 | Parameters
123 | ----------
124 | name : str, optional
125 | The name of the WebsitePool, by default None
126 | allow_duplicates : bool
127 | Whether to allow duplicate websites in the pool
128 | or don't, by default False
129 | """
130 |
131 | self.__sites = list()
132 | self.__allow_duplicates = allow_duplicates
133 | self.__name = None
134 |
135 | self.set_name(name)
136 |
137 | for site in sites:
138 | self.add(site)
139 |
140 | def __str__(self) -> str:
141 | return (f"<{self.__class__.__qualname__}(name={self.__name!r}, "
142 | f"websites={len(self)})>")
143 |
144 | def __len__(self) -> int:
145 | return len(self.__sites)
146 |
147 | def __iter__(self) -> Generator[Website, None, None]:
148 | yield from self.__sites
149 |
150 | def __contains__(self, obj: Any) -> bool:
151 | return isinstance(obj, Website) and any(obj is website for website in self)
152 |
153 | def __copy__(self) -> WebsitePool:
154 | pool = self.__class__.__new__(self.__class__)
155 | pool.__dict__.update(self.__dict__)
156 |
157 | return pool
158 |
159 | def __deepcopy__(self, memo: Dict[int, Any]) -> WebsitePool:
160 | pool = self.__class__.__new__(self.__class__)
161 |
162 | memo[id(self)] = pool
163 |
164 | for k, v in self.__dict__.items():
165 | setattr(pool, k, copy.deepcopy(v, memo))
166 |
167 | return pool
168 |
169 | @property
170 | def sites(self) -> Tuple[Website]:
171 | return tuple(self.__sites)
172 |
173 | @property
174 | def name(self) -> Optional[str]:
175 | return self.__name
176 |
177 | @property
178 | def results(self) -> Tuple[Result]:
179 | return tuple([site.result for site in self if site.result])
180 |
181 | @property
182 | def is_empty(self) -> bool:
183 | return not self
184 |
185 | def set_name(self, name: str) -> None:
186 | """Sets the name for the pool
187 |
188 | Parameters
189 | ----------
190 | name : str
191 | Name for the pool
192 | """
193 |
194 | self.__name = name
195 |
196 | def set_username(self, username: str, /) -> None:
197 | """Sets the username for every website within the pool
198 |
199 | Parameters
200 | ----------
201 | username : str
202 | The username to set for every website
203 | """
204 |
205 | for site in self.sites:
206 | site.set_username(username)
207 |
208 | def add(self, website: Website) -> None:
209 | """Adds a website to the pool
210 |
211 | Parameters
212 | ----------
213 | website : tracer.Website
214 | The website to add to the pool
215 | """
216 |
217 | if not self.__allow_duplicates and website in self:
218 | return None
219 |
220 | self.__sites.append(website)
221 |
222 | def extend(self, pool: WebsitePool, _deepcopy: bool = True) -> None:
223 | """Adds sites from another pool to the own pool
224 |
225 | Parameters
226 | ----------
227 | pool : tracer.WebsitePool
228 | The pool from which to take the sites
229 | _deepcopy : bool
230 | Whether to generate copies of the sites and
231 | add these or add the original sites, by
232 | default True
233 | """
234 |
235 | for site in pool:
236 | self.add(copy.deepcopy(site) if _deepcopy else site)
237 |
238 | def remove(self, where: Callable[[Website], bool]) -> None:
239 | """Removes websites from the pool
240 |
241 | Parameters
242 | ----------
243 | where : Callable[[Website], bool]
244 | A callable which takes in a website object as parameter
245 | and returns a bool. If `True` is returned, then the
246 | website will be removed.
247 | """
248 |
249 | self.__sites = list(filter(lambda w: not where(w), self.sites))
250 |
251 | def get(self, where: Callable[[Website], bool]) -> Tuple[Website]:
252 | """Retrieves websites from the pool
253 |
254 | Parameters
255 | ----------
256 | where : Callable[[Website], bool]
257 | A callable which takes in a website object as parameter
258 | and returns a bool. If `True` is returned, then the
259 | websites will be added to the return tuple.
260 |
261 | Returns
262 | -------
263 | Tuple[Website]
264 | Tuple of websites that were considered True by the
265 | callable
266 | """
267 |
268 | return tuple(filter(where, self))
269 |
270 | def get_by_name(self, name: str, /) -> Optional[Website]:
271 | """Retrieves a website by its name
272 |
273 | Parameters
274 | ----------
275 | name : str
276 | The name of the website to retrieve
277 |
278 | Returns
279 | -------
280 | Optional[Website]
281 | Either the website object or `None` if not
282 | found
283 | """
284 |
285 | result = self.get(lambda w: w.name == name)
286 |
287 | return result[0] if result else None
288 |
289 | async def start_requests(
290 | self,
291 | session: ClientSession,
292 | timeout: Optional[float] = None
293 | ) -> AsyncGenerator[Result, None]:
294 | """Prepares and handles all requests
295 |
296 | Calls `send_request` of every tracer.Website object in the pool and
297 | returns an AsyncGenerator yielding the results of these
298 | requests when available.
299 |
300 | Parameters
301 | ----------
302 | session : aiohttp.ClientSession
303 | A session object which gets used to make the
304 | requests.
305 | timeout : Union[int, float], optional
306 | Represents the time each request has before a
307 | TimeoutError occurs.
308 |
309 | Returns
310 | -------
311 | AsyncGenerator[Result, None]
312 | Yields the results of the requests when available.
313 |
314 | Yields
315 | ------
316 | tracer.Result
317 | The representation of the result of a request
318 | """
319 |
320 | results = asyncio.Queue()
321 | requests = asyncio.gather(*[
322 | site.send_request(session, timeout, cb=results.put) for site in self
323 | ])
324 |
325 | while not (results.empty() and requests.done()):
326 | try:
327 | yield await asyncio.wait_for(results.get(), timeout=0.25)
328 | except asyncio.TimeoutError:
329 | """Force recheck of the loop condition"""
330 |
--------------------------------------------------------------------------------
/tracer/tracer.py:
--------------------------------------------------------------------------------
1 | """Tracer
2 |
3 | This script allows to detect on which websites a username is currently
4 | taken. Chances are that there is always the same person behind a username as
5 | long as the username is special enough.
6 |
7 | Arguments can be provided through the CLI or through the config (.conf) file.
8 |
9 | Dependencies: requirements.txt
10 | """
11 |
12 | __all__ = ("Tracer",)
13 |
14 | from typing import Dict, List, Optional, Union
15 | from threading import Thread
16 | from time import monotonic
17 | from pathlib import Path
18 | import http.cookies
19 | import webbrowser
20 | import asyncio
21 | import json
22 | import os
23 |
24 | from pyvis.network import Network
25 | from aiohttp import ClientSession
26 | from aiohttp.web import run_app
27 | from colorama import Fore
28 | import aiohttp
29 |
30 | from models import *
31 | from loader import *
32 |
33 |
34 | CONFIG = "../settings.conf"
35 | MY_IP = "https://api.myip.com"
36 |
37 |
38 | class Tracer(object):
39 | """Implement the main logic behind Tracer
40 |
41 | Author
42 | ------
43 | chr3st5an
44 | """
45 |
46 | @classmethod
47 | def main(cls) -> None:
48 | """Create a tracer instance and call its run coro
49 |
50 | Parse given args and create an event loop
51 | which executes the `run` coro
52 | """
53 |
54 | # Changing dir so that relative paths make sense
55 | os.chdir(os.path.dirname(__file__))
56 |
57 | # Parse the configs from the conf file and
58 | # update these with the arguments given by the CLI
59 | kwargs = TracerParser(CONFIG).parse()
60 |
61 | try:
62 | loop = asyncio.new_event_loop()
63 | asyncio.set_event_loop(loop)
64 |
65 | if kwargs.get("web"):
66 | from gui import app
67 |
68 | loop.call_later(1, webbrowser.open, "http://127.0.0.1:12345")
69 | run_app(app, host="127.0.0.1", port=12_345, loop=loop)
70 | else:
71 | loop.run_until_complete(cls(kwargs.pop("username"), **kwargs).run())
72 | except KeyboardInterrupt:
73 | print("👋 Bye")
74 | finally:
75 | loop.stop()
76 |
77 | def __init__(
78 | self,
79 | username: str,
80 | data: Optional[List[Dict[str, str]]] = None,
81 | user_agent: Optional[str] = None,
82 | **kwargs,
83 | ):
84 | if data is None:
85 | data = load_website_data()
86 |
87 | if user_agent is None:
88 | user_agent = load_user_agent()
89 |
90 | self.username = username
91 | self.kwargs = dict(kwargs)
92 | self.user_agent = user_agent
93 | self.verbose = kwargs.get("verbose", False)
94 | self.pool = WebsitePool(*[Website.from_dict(data_) for data_ in data])
95 |
96 | self.pool.set_username(self.username)
97 |
98 | self._out_dir = None
99 | self.__filter_sites()
100 |
101 | if kwargs.get("create_file_output"):
102 | self._create_output_dir()
103 |
104 | # When sending a request to TikTok, an annoying message
105 | # is printed by aiohttp. This turns the message off.
106 | http.cookies._is_legal_key = lambda _: True
107 |
108 | def __str__(self) -> str:
109 | return (
110 | f"<{self.__class__.__qualname__}(username={self.username!r}, "
111 | f"kwargs={self.kwargs}, pool={self.pool})>"
112 | )
113 |
114 | def __filter_sites(self) -> None:
115 | """Filter the list of sites based on the given arguments"""
116 |
117 | include: List[str] = self.kwargs.get("only", []) + self.kwargs.get(
118 | "only_category", []
119 | )
120 |
121 | exclude: List[str] = self.kwargs.get("exclude", []) + self.kwargs.get(
122 | "exclude_category", []
123 | )
124 |
125 | if not (include or exclude):
126 | return None
127 |
128 | if exclude:
129 | self.pool.remove(
130 | lambda w: (w.domain in exclude) or (w.category.as_str in exclude)
131 | )
132 |
133 | if include:
134 | self.pool.remove(
135 | lambda w: not ((w.domain in include) or (w.category.as_str in include))
136 | )
137 |
138 | async def run(self) -> None:
139 | """Run the program"""
140 |
141 | if self.kwargs.get("print_logo", True):
142 | print(f"\n{Fore.CYAN}{load_logo()}{Fore.RESET}\n")
143 |
144 | cookie_jar = aiohttp.DummyCookieJar()
145 | headers = {"User-Agent": self.user_agent}
146 |
147 | async with ClientSession(headers=headers, cookie_jar=cookie_jar) as session:
148 | if self.kwargs.get("ip_check"):
149 | await self.retrieve_ip(
150 | session=session, timeout=self.kwargs.get("ip_timeout")
151 | )
152 |
153 | print(f"[{Fore.CYAN}*{Fore.RESET}] Checking {Fore.CYAN}{self.username}"
154 | f"{Fore.RESET} on {len(self.pool)} sites:\n")
155 |
156 | start = monotonic()
157 | counter = 0
158 | requests = self.pool.start_requests(session, self.kwargs.get("timeout"))
159 |
160 | async for response in requests:
161 | message = f"{response.url} {response.verbose() if self.kwargs.get('verbose') else ''}"
162 |
163 | if not response.successfully:
164 | if self.kwargs.get("all"):
165 | if response.timeout:
166 | print(f"{Fore.RED}[Timeout]{Fore.RESET} {message}")
167 | else:
168 | print(f"{Fore.RED}[-]{Fore.RESET} {message}")
169 |
170 | continue
171 |
172 | print(f"{Fore.GREEN}[+]{Fore.RESET} {message}")
173 |
174 | if self.kwargs.get("browse"):
175 | Thread(target=webbrowser.open, args=(response.url,)).start()
176 |
177 | counter += 1
178 |
179 | print(f"\n[{Fore.CYAN}={Fore.RESET}] Found {Fore.CYAN}{counter}"
180 | f"{Fore.RESET} match(es) in {Fore.CYAN}{monotonic() - start:.2f}"
181 | f"s{Fore.RESET}")
182 |
183 | self.write_report(self._out_dir)
184 | self.draw_graph(self._out_dir)
185 |
186 | def write_report(self, out_dir: Union[str, Path]) -> None:
187 | """Create and write a report file which contains the results
188 |
189 | Parameters
190 | ----------
191 | out_dir : Union[str, Path]
192 | In which directory to save the report file. If `None`
193 | is given, then no report file is created
194 | """
195 |
196 | if self._out_dir is None:
197 | return None
198 |
199 | name = f"{out_dir}result.txt"
200 | mode = "w" if os.path.exists(name) else "x"
201 |
202 | with open(name, mode) as file:
203 | file.write(f"{load_logo()}\nReport for {self.username}:\n\n")
204 |
205 | for result in self.pool.results:
206 | file.write(result.url + "\n")
207 |
208 | return None
209 |
210 | def draw_graph(self, out_dir: Optional[Union[str, Path]]) -> None:
211 | """Visualize the results
212 |
213 | Create a HTML file containing a graph and open it
214 | in the default webbrowser
215 |
216 | Parameters
217 | ----------
218 | out_dir : Union[str, Path]
219 | In which directory to save the HTML file. If `None`
220 | is given, then no graph is created
221 | """
222 |
223 | if self._out_dir is None:
224 | return None
225 |
226 | net = Network(
227 | height="100%",
228 | width="100%",
229 | bgcolor="#282a36",
230 | font_color="#f8f8f2",
231 | )
232 |
233 | net.add_node(self.username, color="#ff79c6", title="Username", shape="circle")
234 |
235 | for category in Category.all_categories():
236 | net.add_node(
237 | n_id=category.title(),
238 | color="#bd93f9",
239 | shape="circle",
240 | title=category.title(),
241 | labelHighlightBold=True,
242 | )
243 | net.add_edge(self.username, category.title())
244 |
245 | for site in self.pool:
246 | if site.result.user_exists:
247 | net.add_node(
248 | n_id=site.name,
249 | color="#ff5555",
250 | shape="circle",
251 | title=site.url,
252 | labelHighlightBold=True,
253 | )
254 | net.add_edge(site.category.as_str.title(), site.name)
255 |
256 | # Settings for the graph
257 | net.toggle_physics(True)
258 | net.set_edge_smooth("dynamic")
259 |
260 | # Save the graph in a html file and open it
261 | # in the default browser
262 | net.show(f"{out_dir}graph.html")
263 |
264 | return None
265 |
266 | async def retrieve_ip(
267 | self,
268 | session: ClientSession,
269 | timeout: Optional[float] = None
270 | ) -> None:
271 | """Retrieve the IP address and prints it
272 |
273 | Sleep for 3 seconds after the IP got retrieved.
274 |
275 | Parameters
276 | ----------
277 | session : aiohttp.ClientSession
278 | Session used to send a request
279 | timeout : Union[int, float], optional
280 | Set a timeout for the request, by default None
281 | """
282 |
283 | async def send_request() -> Dict[str, str]:
284 | timeout_ = aiohttp.ClientTimeout(timeout)
285 |
286 | try:
287 | async with session.get(MY_IP, timeout=timeout_) as r:
288 | return json.loads(await r.text())
289 | except asyncio.TimeoutError:
290 | return {"ip": "0.0.0.0 [TIMEOUT]"}
291 |
292 | response = asyncio.create_task(send_request())
293 |
294 | await AsyncTextAnimation("Retrieving IP...", lambda: not response.done())
295 |
296 | print(f"Your IP address is {Fore.CYAN}{response.result()['ip']}{Fore.RESET}\n")
297 |
298 | await asyncio.sleep(3)
299 |
300 | def _create_output_dir(self) -> None:
301 | """Create the directory in which the results are saved"""
302 |
303 | results_dir = "../results/"
304 |
305 | if not os.path.exists(results_dir):
306 | try:
307 | os.mkdir(results_dir)
308 | except PermissionError:
309 | return None
310 |
311 | if not os.path.exists(f"{results_dir}{self.username}"):
312 | os.mkdir(f"{results_dir}{self.username}/")
313 |
314 | self._out_dir = f"{results_dir}{self.username}/"
315 |
316 |
317 | if __name__ == "__main__":
318 | Tracer.main()
319 |
--------------------------------------------------------------------------------
/tracer/models/website.py:
--------------------------------------------------------------------------------
1 | """
2 | MIT License
3 |
4 | Copyright (c) 2022 chr3st5an
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 | """
24 |
25 | from __future__ import annotations
26 |
27 | __all__ = ("Website",)
28 |
29 | from typing import Any, Callable, Coroutine, Dict, Optional, Union
30 | from abc import ABC, abstractmethod
31 | from asyncio import TimeoutError
32 | from time import monotonic
33 | from copy import deepcopy
34 | import asyncio
35 | import re
36 |
37 | from aiohttp import ClientSession, ClientResponse, ClientTimeout
38 |
39 | from .category import Category
40 | from .result import Result
41 |
42 |
43 | class AbstractWebsite(ABC):
44 | @classmethod
45 | @abstractmethod
46 | def from_dict(cls, data):
47 | pass
48 |
49 | @abstractmethod
50 | def set_username(self, username):
51 | pass
52 |
53 | @abstractmethod
54 | def set_result(self, result):
55 | pass
56 |
57 | @abstractmethod
58 | async def send_request(self, session, *args):
59 | pass
60 |
61 |
62 | class Website(AbstractWebsite):
63 | """Represents a website
64 |
65 | Attributes
66 | ----------
67 | name : str
68 | The name of the website, e.g. 'example'
69 | domain : str
70 | The domain name of the website, e.g. 'example.com'
71 | username : str
72 | The username whose existence is being checked on this
73 | website
74 | url : str
75 | The url that represents an users page on this website,
76 | e.g. 'https:\/\/example.com/user/'
77 | true_url : str
78 | The url that gets used to make the HTTP request to this
79 | website
80 | category : int
81 | The category to which the website belongs to
82 | result : tracer.Result
83 | The result of the request, `None` if no request was
84 | started yet
85 | err_ignore_code : bool
86 | Indicates if the returned status code of the request
87 | should be ignored
88 | err_text_pattern : str
89 | Regex pattern that gets applied on the returned text.
90 | If it matches, the username is considered as
91 | 'Not Found'
92 | err_url_pattern : str
93 | Regex pattern that gets applied on the returned url.
94 | If it matches, the username is considered as
95 | 'Not Found'
96 | err_on_dot : bool
97 | Indicates if the website responses with an error
98 | if the username contains a dot. If `True`, no
99 | actual request is send to the website
100 |
101 | Methods
102 | -------
103 | obj.set_username(str)
104 | Sets a username for the website whose existence is later being
105 | checked
106 | obj.set_result(tracer.Result)
107 | Sets a result for the website
108 | obj.send_request(ClientSession, Optional[float], Optional[Callable])
109 | Sends a HTTP GET request to the website and checks if
110 | the username exists. Then creates a `tracer.Result` object
111 | and assigns it to itself by using `obj.set_result`
112 |
113 | Classmethods
114 | ------------
115 | cls.from_dict(dict) -> tracer.Website
116 | Creates a `tracer.Website` object by using the values
117 | of the dict
118 |
119 | Supported Operations
120 | --------------------
121 | `str(obj)`
122 | Returns the str representation of the website
123 | `x == obj`
124 | Compares if `(1):` the other object is a `tracer.Website`
125 | object and if `(2):` the values are the same
126 | `copy.copy(obj)`
127 | Returns a copy of the website
128 | `copy.deepcopy(obj)`
129 | Returns a deepcopy of the website
130 |
131 | Note
132 | ----
133 | The URL(s) passed to an object of this class should
134 | be structured as following `https://www.example.com/path/{}`,
135 | where `{}` represents the field into which the username
136 | is placed
137 |
138 | Author
139 | ------
140 | chr3st5an
141 | """
142 |
143 | @classmethod
144 | def from_dict(cls, data: Dict[str, str]) -> Website:
145 | """Creates a Website by using the given data
146 |
147 | Parameters
148 | ----------
149 | data : Dict[str, str]
150 | A dict whose values are used to create the
151 | Website. The dict must include the following
152 | keys: `domain`, `url` & `category`
153 |
154 | Returns
155 | -------
156 | tracer.Website
157 | The Website that got created
158 | """
159 |
160 | return cls(
161 | domain=data["domain"],
162 | true_url=data["url"],
163 | category=data["category"],
164 | display_url=data.get("display_url"),
165 | err_ignore_code=data.get("err_ignore_code", False),
166 | err_text_pattern=data.get("err_text_pattern"),
167 | err_url_pattern=data.get("err_url_pattern"),
168 | err_on_dot=data.get("err_on_dot", False)
169 | )
170 |
171 | def __init__(
172 | self,
173 | domain: str,
174 | true_url: str,
175 | category: int,
176 | display_url: Optional[str] = None,
177 | err_ignore_code: bool = False,
178 | err_text_pattern: Optional[str] = None,
179 | err_url_pattern: Optional[str] = None,
180 | err_on_dot: bool = False
181 | ):
182 | """Creates an instance
183 |
184 | Parameters
185 | ----------
186 | domain : str
187 | The domain name of the website, e.g. 'example.com'
188 | true_url : str
189 | The url that gets used to make the HTTP request to this
190 | website
191 | category : int
192 | The category to which the website belongs to
193 | display_url : str, optional
194 | The url that represents an users page on this website,
195 | e.g. 'https:\/\/example.com/user/'. If None, then
196 | `true_url` is used, by default None
197 | err_ignore_code : bool
198 | Indicates if the returned status code of the request
199 | should be ignored, by default False
200 | err_text_pattern : str
201 | Regex pattern that gets applied on the returned text.
202 | If it matches, the username is considered as
203 | 'Not Found', by default None
204 | err_url_pattern : str
205 | Regex pattern that gets applied on the returned url.
206 | If it matches, the username is considered as
207 | 'Not Found', by default None
208 | err_on_dot : bool
209 | Indicates if the website responses with an error
210 | if the username contains a dot. If `True`, no
211 | actual request is send to the website, by default
212 | False
213 | """
214 |
215 | self.__url = display_url if display_url else true_url
216 | self.__true_url = true_url
217 | self.__username = None
218 | self.__result = None
219 | self.__category = Category(self, category)
220 | self.__domain = domain
221 |
222 | self.err_ignore_code = err_ignore_code
223 | self.err_text_pattern = err_text_pattern
224 | self.err_url_pattern = err_url_pattern
225 | self.err_on_dot = err_on_dot
226 |
227 | def __str__(self) -> str:
228 | return (f"<{self.__class__.__qualname__}(name={self.name!r}, "
229 | f"domain={self.domain!r}, url={self.url!r}, "
230 | f"category={self.category}, result={self.result})>")
231 |
232 | def __eq__(self, other: Any) -> bool:
233 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
234 |
235 | def __copy__(self) -> Website:
236 | website = self.__class__.__new__(self.__class__)
237 | website.__dict__.update(self.__dict__)
238 |
239 | return website
240 |
241 | def __deepcopy__(self, memo: Dict[int, Any]) -> Website:
242 | website = self.__class__.__new__(self.__class__)
243 |
244 | memo[id(self)] = website
245 |
246 | for k, v in self.__dict__.items():
247 | setattr(website, k, deepcopy(v, memo))
248 |
249 | return website
250 |
251 | @property
252 | def name(self) -> str:
253 | """Return the host name
254 |
255 | `example.com` => `example`
256 | """
257 |
258 | return self.__domain.split('.')[0]
259 |
260 | @property
261 | def domain(self) -> str:
262 | return self.__domain
263 |
264 | @property
265 | def username(self) -> Optional[str]:
266 | return self.__username
267 |
268 | @property
269 | def category(self) -> Category:
270 | return self.__category
271 |
272 | @property
273 | def result(self) -> Optional[Result]:
274 | return self.__result
275 |
276 | @property
277 | def url(self) -> str:
278 | return self.__url.format(self.username) if self.username else self.__url
279 |
280 | @property
281 | def true_url(self) -> str:
282 | return self.__true_url.format(self.username) if self.username else self.__true_url
283 |
284 | def set_username(self, username: str) -> None:
285 | """Sets a username
286 |
287 | Parameters
288 | ----------
289 | username : Optional[str]
290 | The username to set for the website
291 | """
292 |
293 | self.__username = username
294 |
295 | def set_result(self, result: Result) -> None:
296 | """Sets a result
297 |
298 | Parameters
299 | ----------
300 | result : Optional[Result]
301 | The result for the website
302 | """
303 |
304 | self.__result = result
305 |
306 | async def send_request(
307 | self,
308 | session: ClientSession,
309 | timeout: Optional[float] = None,
310 | cb: Optional[Callable[[Result], Union[Coroutine, Any]]] = None
311 | ) -> None:
312 | """Sends a GET requests and evaluates the response
313 |
314 | Parameters
315 | ----------
316 | session : ClientSession
317 | ClientSession to use for the GET request
318 | timeout : Optional[float], optional
319 | How many seconds the request has before a
320 | TimeoutError occurs, by default None
321 | cb : Optional[Callable[[Result], Union[Coroutine, Any]]], optional
322 | Any callable object that gets called when
323 | the result is available. It should only
324 | take in one parameter which is the result,
325 | by default None
326 |
327 | Raises
328 | ------
329 | TypeError
330 | username not set
331 | """
332 |
333 | if self.username is None:
334 | raise TypeError(
335 | "Cannot start request without a username being set"
336 | )
337 |
338 | if "." in self.username and self.err_on_dot:
339 | self.set_result(
340 | result=Result(
341 | website=self,
342 | status_code=400,
343 | successfully=False,
344 | delay=0,
345 | host=self.domain,
346 | url=self.url
347 | )
348 | )
349 |
350 | await self.__callback(cb)
351 |
352 | return None
353 |
354 | timeout = ClientTimeout(timeout)
355 | start = monotonic()
356 |
357 | try:
358 | async with session.get(self.true_url, timeout=timeout) as response:
359 | result = Result(
360 | website=self,
361 | status_code=response.status,
362 | successfully=await self.__user_exists(response),
363 | delay=monotonic() - start,
364 | host=response.host,
365 | url=self.url
366 | )
367 |
368 | response.close()
369 | await response.wait_for_close()
370 | except TimeoutError:
371 | result = Result(
372 | website=self,
373 | status_code=400,
374 | successfully=False,
375 | delay=monotonic() - start,
376 | host=self.domain,
377 | url=self.url,
378 | timeout=True
379 | )
380 | except Exception as e:
381 | result = Result(
382 | website=self,
383 | status_code=600,
384 | successfully=False,
385 | delay=monotonic() - start,
386 | host=self.domain,
387 | url=self.url,
388 | error=e
389 | )
390 | finally:
391 | self.set_result(result)
392 |
393 | await self.__callback(cb)
394 |
395 | return None
396 |
397 | async def __user_exists(self, response: ClientResponse) -> bool:
398 | """Check based on the returned response if the username is in use.
399 |
400 | First check if the response status is 200, then
401 | apply the given regex pattern on the returned text.
402 |
403 | Parameters
404 | ----------
405 | response : aiohttp.ClientResponse
406 | A ClientResponse generated by `aiohttp`.
407 |
408 | Returns
409 | -------
410 | bool
411 | Indicator if the username exists
412 | """
413 |
414 | if not (response.status == 200 or self.err_ignore_code):
415 | return False
416 |
417 | await asyncio.sleep(0)
418 |
419 | if self.err_url_pattern:
420 | if re.search(self.err_url_pattern, str(response.url), flags=re.I):
421 | return False
422 |
423 | if self.err_text_pattern:
424 | text = await response.text()
425 |
426 | if re.search(self.err_text_pattern, text, flags=re.S + re.I + re.M):
427 | return False
428 |
429 | return True
430 |
431 | async def __callback(
432 | self,
433 | callback: Optional[Callable[[Result], Union[Coroutine, Any]]]
434 | ) -> Any:
435 | """Async wrapper for the original callback function"""
436 |
437 | if not callable(callback):
438 | return None
439 |
440 | await asyncio.sleep(0)
441 |
442 | if asyncio.iscoroutinefunction(callback):
443 | return await callback(self.result)
444 |
445 | return callback(self.result)
446 |
--------------------------------------------------------------------------------
/data/pool.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "url": "https://8tracks.com/{}",
4 | "domain": "8tracks.com",
5 | "err_text_pattern": "8tracks radio",
6 | "err_on_dot": true,
7 | "category": 9
8 | },
9 | {
10 | "url": "https://9gag.com/u/{}",
11 | "domain": "9gag.com",
12 | "err_text_pattern": "",
13 | "category": 1
14 | },
15 | {
16 | "url": "https://about.me/{}",
17 | "domain": "about.me",
18 | "err_text_pattern": "about\\.meAcademia\\.edu",
25 | "err_on_dot": true,
26 | "category": 16
27 | },
28 | {
29 | "url": "https://community.airtable.com/u/{}",
30 | "domain": "airtable.com",
31 | "err_text_pattern": "Airtable Community.*?",
32 | "category": 16
33 | },
34 | {
35 | "url": "https://aminoapps.com/u/{}",
36 | "domain": "aminoapps.com",
37 | "err_text_pattern": "Amino",
38 | "category": 4
39 | },
40 | {
41 | "url": "https://www.answers.com/u/{}",
42 | "domain": "answers.com",
43 | "err_text_pattern": "404 Not Found.*?",
44 | "category": 9
45 | },
46 | {
47 | "url": "https://archive.org/details/@{}",
48 | "domain": "archive.org",
49 | "err_text_pattern": "cannot find.*?",
50 | "category": 16
51 | },
52 | {
53 | "url": "https://ask.fm/{}",
54 | "domain": "ask.fm",
55 | "err_text_pattern": "Ask\\sand\\sAnswer.*?",
56 | "category": 7
57 | },
58 | {
59 | "url": "https://bandcamp.com/{}",
60 | "domain": "bandcamp.com",
61 | "err_text_pattern": "Bandcamp",
62 | "category": 9
63 | },
64 | {
65 | "url": "https://beacons.ai/{}",
66 | "domain": "beacons.ai",
67 | "err_text_pattern": "Beacons",
68 | "category": 16
69 | },
70 | {
71 | "url": "https://br.bebee.com/bee/{}",
72 | "domain": "bebee.com",
73 | "err_text_pattern": "class=\\\"text-danger bb-expired\\\"",
74 | "category": 16
75 | },
76 | {
77 | "url": "https://www.behance.net/{}",
78 | "domain": "behance.net",
79 | "err_text_pattern": "Behance ::.+?",
80 | "category": 4
81 | },
82 | {
83 | "url": "https://bigo.tv/en/{}",
84 | "domain": "bigo.tv",
85 | "err_text_pattern": "ignoreUids:\\[\\]",
86 | "category": 6
87 | },
88 | {
89 | "url": "https://blip.fm/{}",
90 | "domain": "blip.fm",
91 | "err_text_pattern": "404.*?Page Not Found",
92 | "category": 9
93 | },
94 | {
95 | "url": "https://{}.blogspot.com/",
96 | "domain": "blogspot.com",
97 | "err_text_pattern": "https://www\\.blogger\\.com/create-blog\\.g?defaultSubdomain=",
98 | "err_on_dot": true,
99 | "category": 3
100 | },
101 | {
102 | "url": "https://www.buymeacoffee.com/{}",
103 | "domain": "buymeacoffee.com",
104 | "err_text_pattern": "Buy Me a Coffee -.*?",
105 | "category": 16
106 | },
107 | {
108 | "url": "https://www.buzzfeed.com/{}",
109 | "domain": "buzzfeed.com",
110 | "err_text_pattern": "Page not found",
111 | "category": 16
112 | },
113 | {
114 | "url": "https://api.caffeine.tv/v1/users/{}",
115 | "display_url": "https://www.caffeine.tv/{}/profile/",
116 | "domain": "caffeine.tv",
117 | "err_text_pattern": "\"_account\":[\"Could not be found.\"]",
118 | "err_on_dot": true,
119 | "category": 6
120 | },
121 | {
122 | "url": "https://campsite.bio/{}",
123 | "domain": "campsite.bio",
124 | "err_text_pattern": "Campsite",
125 | "category": 15
126 | },
127 | {
128 | "url": "https://www.canva.com/p/{}/",
129 | "domain": "canva.com",
130 | "err_text_pattern": "Amazingly Simple Graphic.*?",
131 | "category": 4
132 | },
133 | {
134 | "url": "https://www.caringbridge.org/visit/{}",
135 | "domain": "caringbridge.org",
136 | "err_text_pattern": "CaringBridge ",
137 | "err_ignore_code": true,
138 | "category": 16
139 | },
140 | {
141 | "url": "https://cash.app/${}",
142 | "domain": "cash.app",
143 | "err_text_pattern": "",
144 | "category": 16
145 | },
146 | {
147 | "url": "https://chaturbate.com/{}/",
148 | "domain": "chaturbate.com",
149 | "err_text_pattern": "HTTP 404.*? ",
150 | "err_url_pattern": "(?:chaturbate\\.[a-z]+?\\/?)$",
151 | "category": 2
152 | },
153 | {
154 | "url": "https://www.chess.com/member/{}",
155 | "domain": "chess.com",
156 | "err_text_pattern": "",
157 | "category": 14
158 | },
159 | {
160 | "url": "https://www.codechef.com/users/{}",
161 | "domain": "codechef.com",
162 | "err_url_pattern": "(?:codechef\\.[a-zA-Z]+?\\/?)$",
163 | "category": 5
164 | },
165 | {
166 | "url": "https://codepen.io/{}",
167 | "domain": "codepen.io",
168 | "err_text_pattern": "404 on.*?",
169 | "category": 5
170 | },
171 | {
172 | "url": "https://codeforces.com/profile/{}",
173 | "domain": "codeforces.com",
174 | "err_url_pattern": "(?:codeforces\\.[a-z]+?\\/?)$",
175 | "err_on_dot": true,
176 | "category": 5
177 | },
178 | {
179 | "url": "https://coderwall.com/{}",
180 | "domain": "coderwall.com",
181 | "err_text_pattern": "",
182 | "err_on_dot": true,
183 | "category": 5
184 | },
185 | {
186 | "url": "https://codewars.com/users/{}",
187 | "domain": "codewars.com",
188 | "err_text_pattern": "",
189 | "category": 5
190 | },
191 | {
192 | "url": "https://creativemarket.com/users/{}",
193 | "domain": "creativemarket.com",
194 | "err_text_pattern": "Whoomp, there.*?",
195 | "category": 4
196 | },
197 | {
198 | "url": "https://crokes.com/{}/",
199 | "domain": "crokes.com",
200 | "err_text_pattern": "Page Not Found.*?",
201 | "category": 1
202 | },
203 | {
204 | "url": "https://www.crunchyroll.com/user/{}",
205 | "domain": "crunchyroll.com",
206 | "err_text_pattern": "Crunchyroll - Page not found ",
207 | "category": 1
208 | },
209 | {
210 | "url": "https://curiouscat.live/api/v2.1/profile?username={}",
211 | "display_url": "https://curiouscat.live/{}",
212 | "domain": "curiouscat.live",
213 | "err_text_pattern": "\\\"profile_does_not_exist\\\"",
214 | "category": 7
215 | },
216 | {
217 | "url": "https://dev.to/{}",
218 | "domain": "dev.to",
219 | "err_text_pattern": "The page you.*?",
220 | "err_on_dot": true,
221 | "category": 5
222 | },
223 | {
224 | "url": "https://deviantart.com/{}",
225 | "domain": "deviantart.com",
226 | "err_text_pattern": "DeviantArt: 404",
227 | "category": 4
228 | },
229 | {
230 | "url": "https://disqus.com/by/{}/?",
231 | "domain": "disqus.com",
232 | "err_text_pattern": "Page not found.*?",
233 | "category": 16
234 | },
235 | {
236 | "url": "https://dribbble.com/{}/about",
237 | "domain": "dribbble.com",
238 | "err_text_pattern": "Sorry, the page.*?",
239 | "category": 4
240 | },
241 | {
242 | "url": "https://dzone.com/users/{}",
243 | "domain": "dzone.com",
244 | "err_url_pattern": "\\/users\\/(?:[^/?=]+?\\/?)$",
245 | "err_on_dot": true,
246 | "category": 5
247 | },
248 | {
249 | "url": "https://ebay.com/usr/{}",
250 | "domain": "ebay.com",
251 | "err_text_pattern": "ebay profile - error.*?",
252 | "category": 16
253 | },
254 | {
255 | "url": "https://ello.co/{}",
256 | "domain": "ello.co",
257 | "err_text_pattern": ".+?\\[404\\] Not Found",
258 | "category": 4
259 | },
260 | {
261 | "url": "https://www.eporner.com/profile/{}/",
262 | "domain": "eporner.com",
263 | "err_text_pattern": "",
264 | "category": 2
265 | },
266 | {
267 | "url": "https://www.etsy.com/people/{}",
268 | "domain": "etsy.com",
269 | "err_text_pattern": "Etsy -.*?",
270 | "category": 4
271 | },
272 | {
273 | "url": "https://www.experts-exchange.com/members/{}",
274 | "domain": "experts-exchange.com",
275 | "err_text_pattern": "Not Found",
276 | "err_on_dot": true,
277 | "category": 16
278 | },
279 | {
280 | "url": "https://facebook.com/{}",
281 | "domain": "facebook.com",
282 | "err_text_pattern": " ",
289 | "category": 3
290 | },
291 | {
292 | "url": "https://fanlink.to/{}",
293 | "domain": "fanlink.to",
294 | "err_text_pattern": "ToneDen -.*?",
295 | "category": 15
296 | },
297 | {
298 | "url": "https://www.fark.com/users/{}",
299 | "domain": "fark.com",
300 | "err_text_pattern": "FARK\\.com: User profiles: view ",
301 | "category": 16
302 | },
303 | {
304 | "url": "https://www.fiverr.com/{}",
305 | "domain": "fiverr.com",
306 | "err_text_pattern": "Fiverr -.*?",
307 | "err_url_pattern": "(?:fiverr\\.[a-z]+?\\/?)$",
308 | "category": 16
309 | },
310 | {
311 | "url": "https://flickr.com/photos/{}",
312 | "domain": "flickr.com",
313 | "err_text_pattern": "404 ",
314 | "category": 4
315 | },
316 | {
317 | "url": "https://flipboard.com/@{}",
318 | "domain": "flipboard.com",
319 | "err_text_pattern": "Flipboard:.+?",
320 | "category": 13
321 | },
322 | {
323 | "url": "https://members.fotki.com/{}/about/",
324 | "domain": "fotki.com",
325 | "err_text_pattern": "Member Not Found.*?",
326 | "err_on_dot": true,
327 | "category": 1
328 | },
329 | {
330 | "url": "https://forum.freecodecamp.org/u/{}",
331 | "domain": "freecodecamp.org",
332 | "err_text_pattern": "The freeCodeCamp.*?",
333 | "category": 5
334 | },
335 | {
336 | "url": "https://www.gaiaonline.com/profiles/{}/",
337 | "domain": "gaiaonline.com",
338 | "err_text_pattern": "",
339 | "err_on_dot": true,
340 | "category": 14
341 | },
342 | {
343 | "url": "https://auth.geeksforgeeks.org/user/{}/profile",
344 | "domain": "geeksforgeeks.org",
345 | "err_text_pattern": "Login.*?",
346 | "err_url_pattern": ".*?\\?to=.*?",
347 | "category": 5
348 | },
349 | {
350 | "url": "https://meta.getaether.net/u/{}",
351 | "domain": "getaether.net",
352 | "err_text_pattern": "Meta Aether",
353 | "category": 1
354 | },
355 | {
356 | "url": "https://giphy.com/{}",
357 | "domain": "giphy.com",
358 | "err_text_pattern": "(?:404 Not Found)|(?:)",
359 | "err_url_pattern": ".*?\\/explore\\/.*?",
360 | "category": 11
361 | },
362 | {
363 | "url": "https://gitea.com/{}",
364 | "domain": "gitea.com",
365 | "err_text_pattern": " Page Not Found.*?",
366 | "category": 5
367 | },
368 | {
369 | "url": "https://gitee.com/{}",
370 | "domain": "gitee.com",
371 | "err_text_pattern": "",
372 | "category": 5
373 | },
374 | {
375 | "url": "https://github.com/{}",
376 | "domain": "github.com",
377 | "err_text_pattern": "Page not found.*?",
378 | "category": 5
379 | },
380 | {
381 | "url": "https://gitlab.com/{}",
382 | "domain": "gitlab.com",
383 | "err_text_pattern": "Sign in.*?",
384 | "category": 5
385 | },
386 | {
387 | "url": "https://www.girlsaskguys.com/user/{}",
388 | "domain": "girlsaskguys.com",
389 | "err_text_pattern": "This doesn.*?",
390 | "category": 1
391 | },
392 | {
393 | "url": "https://www.goodreads.com/{}",
394 | "domain": "goodreads.com",
395 | "err_text_pattern": "Page not found",
396 | "category": 16
397 | },
398 | {
399 | "url": "https://en.gravatar.com/{}",
400 | "domain": "gravatar.com",
401 | "err_text_pattern": "Gravatar.*?",
402 | "err_url_pattern": "profiles\\/no-such-user",
403 | "err_on_dot": true,
404 | "category": 16
405 | },
406 | {
407 | "url": "https://{}.gumroad.com/",
408 | "domain": "gumroad.com",
409 | "err_text_pattern": "Page not found.+?",
410 | "err_on_dot": true,
411 | "category": 16
412 | },
413 | {
414 | "url": "https://www.gutefrage.net/nutzer/{}",
415 | "domain": "gutefrage.net",
416 | "err_text_pattern": "Gutefrage.*?",
417 | "category": 16
418 | },
419 | {
420 | "url": "https://gfycat.com/@{}",
421 | "domain": "gfycat.com",
422 | "err_text_pattern": "Page not found.*?",
423 | "category": 16
424 | },
425 | {
426 | "url": "https://hackerone.com/{}?type=user",
427 | "display_url": "https://hackerone.com/{}",
428 | "domain": "hackerone.com",
429 | "err_text_pattern": "Page not found ",
430 | "err_on_dot": true,
431 | "category": 5
432 | },
433 | {
434 | "url": "https://{}.hashnode.dev",
435 | "domain": "hashnode.com",
436 | "err_text_pattern": "404",
437 | "err_on_dot": true,
438 | "category": 5
439 | },
440 | {
441 | "url": "https://hubpages.com/@{}",
442 | "domain": "hubpages.com",
443 | "err_text_pattern": "Page Not Found",
444 | "err_on_dot": true,
445 | "category": 3
446 | },
447 | {
448 | "url": "https://hypel.ink/{}",
449 | "domain": "hypel.ink",
450 | "err_text_pattern": "Hype Link",
451 | "category": 15
452 | },
453 | {
454 | "url": "https://ifttt.com/p/{}",
455 | "domain": "ifttt.com",
456 | "err_text_pattern": "",
457 | "err_on_dot": true,
458 | "category": 16
459 | },
460 | {
461 | "url": "https://ifunny.co/user/{}",
462 | "domain": "ifunny.co",
463 | "err_text_pattern": "404 - page.*?",
464 | "category": 11
465 | },
466 | {
467 | "url": "https://imageshack.com/user/{}",
468 | "domain": "imageshack.com",
469 | "err_url_pattern": "imageshack\\.(?:[a-z]+?\\/?)$",
470 | "category": 1
471 | },
472 | {
473 | "url": "https://imgur.com/search?q=user:{}",
474 | "display_url": "https://imgur.com/user/{}",
475 | "domain": "imgur.com",
476 | "err_text_pattern": "Found.*?0 .*?results for",
477 | "err_url_pattern": "search\\?q=.*?",
478 | "err_on_dot": true,
479 | "category": 11
480 | },
481 | {
482 | "url": "https://instabio.cc/{}",
483 | "domain": "instabio.cc",
484 | "err_text_pattern": "",
485 | "category": 15
486 | },
487 | {
488 | "url": "https://instagram.com/{}/guides",
489 | "display_url": "https://instagram.com/{}",
490 | "domain": "instagram.com",
491 | "err_text_pattern": "Instagram",
492 | "category": 1
493 | },
494 | {
495 | "url": "https://issuu.com/{}",
496 | "domain": "issuu.com",
497 | "err_text_pattern": "Issuu - Page.*?",
498 | "category": 3
499 | },
500 | {
501 | "url": "https://itch.io/profile/{}",
502 | "domain": "itch.io",
503 | "err_text_pattern": "itch\\.io",
504 | "category": 14
505 | },
506 | {
507 | "url": "https://{}.jimdosite.com/",
508 | "domain": "jimdosite.com",
509 | "err_text_pattern": "",
510 | "err_on_dot": true,
511 | "category": 16
512 | },
513 | {
514 | "url": "https://community.k6.io/u/{}",
515 | "domain": "k6.io",
516 | "err_text_pattern": "k6 community.*?",
517 | "category": 16
518 | },
519 | {
520 | "url": "https://www.kaggle.com/{}",
521 | "domain": "kaggle.com",
522 | "err_text_pattern": "Kaggle: Your Home for Data Science",
523 | "err_on_dot": true,
524 | "category": 5
525 | },
526 | {
527 | "url": "https://ws2.kik.com/user/{}",
528 | "domain": "kik.com",
529 | "err_text_pattern": "Not Found",
530 | "category": 7
531 | },
532 | {
533 | "url": "https://last.fm/user/{}",
534 | "domain": "last.fm",
535 | "err_text_pattern": "Page\\sNot\\sFound.*?",
536 | "category": 9
537 | },
538 | {
539 | "url": "https://letterboxd.com/{}/",
540 | "domain": "letterboxd.com",
541 | "err_text_pattern": "Letterboxd.*?",
542 | "category": 6
543 | },
544 | {
545 | "url": "https://letterpile.com/@{}",
546 | "domain": "letterpile.com",
547 | "err_text_pattern": "LetterPile",
548 | "category": 3
549 | },
550 | {
551 | "url": "https://lichess.org/@/{}",
552 | "domain": "lichess.org",
553 | "err_text_pattern": "",
554 | "category": 14
555 | },
556 | {
557 | "url": "https://www.liinks.co/{}",
558 | "domain": "liinks.co",
559 | "err_text_pattern": "Liinks.*?",
560 | "err_url_pattern": "type=NO_USER",
561 | "category": 15
562 | },
563 | {
564 | "url": "https://line.me/R/ti/p/@{}",
565 | "domain": "line.me",
566 | "err_text_pattern": "",
567 | "category": 7
568 | },
569 | {
570 | "url": "https://linkedin.com/in/{}",
571 | "domain": "linkedin.com",
572 | "err_url_pattern": "\\.[a-z]+?\\/authwall\\?.*?",
573 | "category": 16
574 | },
575 | {
576 | "url": "https://linkfly.to/{}",
577 | "domain": "linkfly.to",
578 | "err_text_pattern": "404 - Oops.*?",
579 | "category": 15
580 | },
581 | {
582 | "url": "https://linktr.ee/{}",
583 | "domain": "linktr.ee",
584 | "err_text_pattern": "\",\"statusCode\":404",
585 | "category": 15
586 | },
587 | {
588 | "url": "https://{}.livejournal.com/",
589 | "domain": "livejournal.com",
590 | "err_text_pattern": "",
591 | "err_on_dot": true,
592 | "category": 3
593 | },
594 | {
595 | "url": "https://lnk.bio/{}",
596 | "domain": "lnk.bio",
597 | "err_text_pattern": "Not Found.*?",
598 | "category": 15
599 | },
600 | {
601 | "url": "https://mastodon.social/@{}",
602 | "domain": "mastodon.social",
603 | "err_text_pattern": "The page you.*?",
604 | "err_on_dot": true,
605 | "category": 1
606 | },
607 | {
608 | "url": "https://medium.com/@{}",
609 | "domain": "medium.com",
610 | "err_text_pattern": "Medium",
611 | "category": 1
612 | },
613 | {
614 | "url": "https://meetme.com/{}",
615 | "domain": "meetme.com",
616 | "err_text_pattern": "MeetMe.*?",
617 | "err_on_dot": true,
618 | "category": 16
619 | },
620 | {
621 | "url": "https://www.memecenter.com/{}",
622 | "domain": "memecenter.com",
623 | "err_text_pattern": "Meme Center -.*?",
624 | "category": 11
625 | },
626 | {
627 | "url": "https://micro.blog/{}",
628 | "domain": "micro.blog",
629 | "err_text_pattern": "User not found",
630 | "category": 3
631 | },
632 | {
633 | "url": "https://mix.com/{}",
634 | "domain": "mix.com",
635 | "err_text_pattern": "Mix.*?",
636 | "err_on_dot": true,
637 | "category": 1
638 | },
639 | {
640 | "url": "https://myanimelist.net/profile/{}",
641 | "domain": "myanimelist.net",
642 | "err_text_pattern": "404 Not.*?",
643 | "err_on_dot": true,
644 | "category": 16
645 | },
646 | {
647 | "url": "https://myspace.com/{}",
648 | "domain": "myspace.com",
649 | "err_text_pattern": "Page Not Found ",
650 | "category": 1
651 | },
652 | {
653 | "url": "https://ngl.link/{}",
654 | "domain": "ngl.link",
655 | "err_text_pattern": "Could not find user",
656 | "category": 16
657 | },
658 | {
659 | "url": "https://www.npmjs.com/~{}",
660 | "domain": "npmjs.com",
661 | "err_text_pattern": "not found",
662 | "category": 5
663 | },
664 | {
665 | "url": "https://community.octoprint.org/u/{}",
666 | "domain": "octoprint.org",
667 | "err_text_pattern": "OctoPrint.*?",
668 | "category": 16
669 | },
670 | {
671 | "url": "https://odysee.com/@{}",
672 | "domain": "odysee.com",
673 | "err_text_pattern": "Odysee",
674 | "category": 6
675 | },
676 | {
677 | "url": "https://omlet.gg/profile/{}",
678 | "domain": "omlet.gg",
679 | "err_text_pattern": "Omlet Arcade ",
680 | "category": 6
681 | },
682 | {
683 | "url": "https://pastebin.com/u/{}",
684 | "domain": "pastebin.com",
685 | "err_text_pattern": ".+?Not Found.*?",
686 | "category": 5
687 | },
688 | {
689 | "url": "https://www.patreon.com/{}/creators",
690 | "display_url": "https://www.patreon.com/{}",
691 | "domain": "patreon.com",
692 | "err_text_pattern": "404",
693 | "category": 16
694 | },
695 | {
696 | "url": "https://www.pexels.com/@{}",
697 | "domain": "pexels.com",
698 | "err_text_pattern": "Error 404.*?",
699 | "category": 16
700 | },
701 | {
702 | "url": "https://picsart.com/u/{}",
703 | "domain": "picsart.com",
704 | "err_text_pattern": "Page not found",
705 | "category": 1
706 | },
707 | {
708 | "url": "https://pinterest.com/{}",
709 | "domain": "pinterest.com",
710 | "err_url_pattern": "(?:\/ideas\/)$",
711 | "err_text_pattern": "",
712 | "category": 4
713 | },
714 | {
715 | "url": "https://pixabay.com/users/{}/",
716 | "domain": "pixabay.com",
717 | "err_text_pattern": "Error 404",
718 | "err_on_dot": true,
719 | "category": 16
720 | },
721 | {
722 | "url": "https://www.plurk.com/{}",
723 | "domain": "plurk.com",
724 | "err_text_pattern": "User Not Found.*?",
725 | "category": 1
726 | },
727 | {
728 | "url": "https://pornhub.com/users/{}",
729 | "domain": "pornhub.com",
730 | "err_text_pattern": "Page Not Found",
731 | "category": 2
732 | },
733 | {
734 | "url": "https://www.prezi.community/u/{}",
735 | "domain": "prezi.community",
736 | "err_text_pattern": "",
737 | "category": 12
738 | },
739 | {
740 | "url": "https://www.producthunt.com/@{}",
741 | "domain": "producthunt.com",
742 | "err_text_pattern": "Product Hunt:.*?",
743 | "category": 16
744 | },
745 | {
746 | "url": "https://www.polywork.com/{}",
747 | "domain": "polywork.com",
748 | "err_text_pattern": "The page you were looking.*?",
749 | "err_on_dot": true,
750 | "category": 16
751 | },
752 | {
753 | "url": "https://www.pscp.tv/{}",
754 | "domain": "pscp.tv",
755 | "err_text_pattern": "404",
756 | "category": 16
757 | },
758 | {
759 | "url": "https://pypi.org/user/{}/",
760 | "domain": "pypi.org",
761 | "err_text_pattern": "Page Not Found.*?",
762 | "category": 5
763 | },
764 | {
765 | "url": "https://quizlet.com/{}",
766 | "domain": "quizlet.com",
767 | "err_text_pattern": "",
768 | "err_url_pattern": "\\/\\w+?\\/\\d{9}",
769 | "category": 16
770 | },
771 | {
772 | "url": "https://quora.com/profile/{}",
773 | "domain": "quora.com",
774 | "err_text_pattern": "",
775 | "category": 16
776 | },
777 | {
778 | "url": "https://redbubble.com/people/{}",
779 | "domain": "redbubble.com",
780 | "err_text_pattern": "This is a lost cause\\. ",
781 | "category": 4
782 | },
783 | {
784 | "url": "https://reddit.com/user/{}",
785 | "domain": "reddit.com",
786 | "err_text_pattern": "Sorry, nobody.*?name\\.",
787 | "err_on_dot": true,
788 | "category": 1
789 | },
790 | {
791 | "url": "https://www.redgifs.com/users/{}",
792 | "domain": "redgifs.com",
793 | "err_text_pattern": "Page Not Found",
794 | "category": 2
795 | },
796 | {
797 | "url": "https://redtube.com/users/{}",
798 | "domain": "redtube.com",
799 | "err_text_pattern": "Page Not Found",
800 | "category": 2
801 | },
802 | {
803 | "url": "https://replit.com/@{}",
804 | "domain": "replit.com",
805 | "err_text_pattern": "Replit - 404 - Replit",
806 | "category": 5
807 | },
808 | {
809 | "url": "https://www.reverbnation.com/{}",
810 | "domain": "reverbnation.com",
811 | "err_text_pattern": "",
812 | "category": 9
813 | },
814 | {
815 | "url": "https://sharesome.com/api/users/{}?stats=1&profile=1",
816 | "display_url": "https://sharesome.com/{}",
817 | "domain": "sharesome.com",
818 | "err_text_pattern": "404 \\|.*?",
819 | "err_on_dot": true,
820 | "category": 2
821 | },
822 | {
823 | "url": "https://shoptly.com/{}",
824 | "domain": "shoptly.com",
825 | "err_text_pattern": "Sell Digital.*?",
826 | "category": 16
827 | },
828 | {
829 | "url": "https://{}.skyrock.com/profil/",
830 | "domain": "skyrock.com",
831 | "err_text_pattern": "",
832 | "err_on_dot": true,
833 | "category": 3
834 | },
835 | {
836 | "url": "https://{}.slack.com/",
837 | "domain": "slack.com",
838 | "err_text_pattern": "There's been a gli.*?",
839 | "err_on_dot": true,
840 | "category": 7
841 | },
842 | {
843 | "url": "https://www.slideshare.net/{}",
844 | "domain": "slideshare.net",
845 | "err_text_pattern": "Username available",
846 | "category": 12
847 | },
848 | {
849 | "url": "https://snapchat.com/add/{}",
850 | "domain": "snapchat.com",
851 | "err_text_pattern": "Sorry,.*?not\\sfound",
852 | "category": 1
853 | },
854 | {
855 | "url": "https://soundcloud.com/{}",
856 | "domain": "soundcloud.com",
857 | "err_text_pattern": "Something went wrong.*?",
858 | "category": 9
859 | },
860 | {
861 | "url": "https://sourceforge.net/u/{}/profile/",
862 | "domain": "sourceforge.net",
863 | "err_text_pattern": "Page not found.*?",
864 | "category": 5
865 | },
866 | {
867 | "url": "https://spankbang.com/profile/{}",
868 | "domain": "spankbang.com",
869 | "err_text_pattern": "",
870 | "category": 2
871 | },
872 | {
873 | "url": "https://spreely.com/{}",
874 | "domain": "spreely.com",
875 | "err_text_pattern": "Spreely.*?",
876 | "category": 1
877 | },
878 | {
879 | "url": "https://open.spotify.com/user/{}",
880 | "domain": "spotify.com",
881 | "err_text_pattern": "Page not found",
882 | "category": 9
883 | },
884 | {
885 | "url": "https://{}.start.page/",
886 | "domain": "start.page",
887 | "err_text_pattern": "Page not found!",
888 | "err_on_dot": true,
889 | "category": 16
890 | },
891 | {
892 | "url": "https://steamcommunity.com/id/{}",
893 | "domain": "steamcommunity.com",
894 | "err_text_pattern": ".*?::\\s*?Error.*?",
895 | "category": 14
896 | },
897 | {
898 | "url": "https://steemit.com/@{}",
899 | "domain": "steemit.com",
900 | "err_text_pattern": "Page Not Found.*?",
901 | "category": 1
902 | },
903 | {
904 | "url": "https://www.strava.com/athletes/{}",
905 | "domain": "strava.com",
906 | "err_text_pattern": "",
907 | "err_on_dot": true,
908 | "category": 10
909 | },
910 | {
911 | "url": "https://{}.substack.com/",
912 | "domain": "substack.com",
913 | "err_text_pattern": "Discover Substack.*?",
914 | "err_url_pattern": "\\.\\[a-z]+?\\/discover",
915 | "err_on_dot": true,
916 | "category": 16
917 | },
918 | {
919 | "url": "https://www.taringa.net/{}",
920 | "domain": "taringa.net",
921 | "err_text_pattern": "Taringa!.*?",
922 | "category": 1
923 | },
924 | {
925 | "url": "https://t.me/{}",
926 | "domain": "telegram.com",
927 | "err_text_pattern": " ",
928 | "err_url_pattern": "telegram\\.org",
929 | "category": 7
930 | },
931 | {
932 | "url": "https://tellonym.me/{}",
933 | "domain": "tellonym.me",
934 | "err_text_pattern": "TextBin",
941 | "err_on_dot": true,
942 | "category": 5
943 | },
944 | {
945 | "url": "https://tiktok.com/@{}",
946 | "domain": "tiktok.com",
947 | "err_text_pattern": "Couldn't find this account.*?",
948 | "category": 1
949 | },
950 | {
951 | "url": "https://tinder.com/@{}",
952 | "domain": "tinder.com",
953 | "err_text_pattern": "(?:Tinder.*?)?",
954 | "category": 8
955 | },
956 | {
957 | "url": "https://www.tripadvisor.com/Profile/{}",
958 | "domain": "tripadvisor.com",
959 | "err_text_pattern": "",
960 | "category": 16
961 | },
962 | {
963 | "url": "https://{}.tumblr.com/",
964 | "domain": "tumblr.com",
965 | "err_text_pattern": "",
966 | "err_on_dot": true,
967 | "category": 1
968 | },
969 | {
970 | "url": "https://twitch.tv/{}",
971 | "domain": "twitch.tv",
972 | "err_text_pattern": "content='@twitch'> Page not found.*?",
979 | "category": 16
980 | },
981 | {
982 | "url": "https://untappd.com/user/{}",
983 | "domain": "untappd.com",
984 | "err_text_pattern": "",
985 | "category": 16
986 | },
987 | {
988 | "url": "https://valence.community/visualization/profile/{}",
989 | "domain": "valence.community",
990 | "err_text_pattern": "",
991 | "category": 16
992 | },
993 | {
994 | "url": "https://www.vice.com/en/contributor/{}",
995 | "domain": "vice.com",
996 | "err_text_pattern": "vice\\.com.*?",
997 | "category": 16
998 | },
999 | {
1000 | "url": "https://vimeo.com/{}",
1001 | "domain": "vimeo.com",
1002 | "err_text_pattern": "",
1003 | "category": 6
1004 | },
1005 | {
1006 | "url": "https://vk.com/{}",
1007 | "domain": "vk.com",
1008 | "err_text_pattern": "404 Not.*?",
1009 | "category": 1
1010 | },
1011 | {
1012 | "url": "https://vsco.co/{}/gallery",
1013 | "domain": "vsco.co",
1014 | "err_text_pattern": "Page not found.*?",
1015 | "category": 1
1016 | },
1017 | {
1018 | "url": "https://community.w2g.tv/u/{}",
1019 | "domain": "w2g.tv",
1020 | "err_text_pattern": "Watch2Gether.*?",
1021 | "category": 16
1022 | },
1023 | {
1024 | "url": "https://wakatime.com/@{}",
1025 | "domain": "wakatime.com",
1026 | "err_text_pattern": "404: Not Found - WakaTime",
1027 | "category": 5
1028 | },
1029 | {
1030 | "url": "https://wattpad.com/user/{}",
1031 | "domain": "wattpad.com",
1032 | "err_text_pattern": "Wattpad -.*?",
1033 | "category": 4
1034 | },
1035 | {
1036 | "url": "https://{}.webflow.io/",
1037 | "domain": "webflow.com",
1038 | "err_text_pattern": "404 -.*?",
1039 | "err_on_dot": true,
1040 | "category": 16
1041 | },
1042 | {
1043 | "url": "https://{}.webnode.com/",
1044 | "domain": "webnode.com",
1045 | "err_text_pattern": "We are sorry, but the page .*?\\.webnode\\.com",
1046 | "err_on_dot": true,
1047 | "category": 3
1048 | },
1049 | {
1050 | "url": "https://{}.weebly.com/",
1051 | "domain": "weebly.com",
1052 | "err_text_pattern": "404 - Page.*?",
1053 | "err_on_dot": true,
1054 | "category": 3
1055 | },
1056 | {
1057 | "url": "https://weheartit.com/{}",
1058 | "domain": "weheartit.com",
1059 | "err_text_pattern": "The Page.*?",
1060 | "category": 1
1061 | },
1062 | {
1063 | "url": "https://wikipedia.org/wiki/User:{}",
1064 | "domain": "wikipedia.org",
1065 | "err_text_pattern": "\\\".*?\\\" is not registered on this wiki.*?",
1066 | "category": 16
1067 | },
1068 | {
1069 | "url": "https://{}.wordpress.com",
1070 | "domain": "wordpress.com",
1071 | "err_url_pattern": "\\/typo\\/\\?subdomain=.+?",
1072 | "err_on_dot": true,
1073 | "category": 5
1074 | },
1075 | {
1076 | "url": "https://wt.social/u/{}",
1077 | "domain": "wt.social",
1078 | "err_text_pattern": "",
1079 | "category": 1
1080 | },
1081 | {
1082 | "url": "https://xhamster.com/users/{}",
1083 | "domain": "xhamster.com",
1084 | "err_text_pattern": "User not found",
1085 | "category": 2
1086 | },
1087 | {
1088 | "url": "https://xvideos.com/profiles/{}",
1089 | "domain": "xvideos.com",
1090 | "err_text_pattern": "",
1091 | "category": 2
1092 | },
1093 | {
1094 | "url": "https://news.ycombinator.com/user?id={}",
1095 | "domain": "ycombinator.com",
1096 | "err_text_pattern": "No such user\\.",
1097 | "category": 3
1098 | },
1099 | {
1100 | "url": "https://{}.yelp.com/",
1101 | "domain": "yelp.com",
1102 | "err_url_pattern": "yelp\\.[a-z]+?/[^/?=]+$",
1103 | "err_on_dot": true,
1104 | "err_text_pattern": "Restaurants, Dentists, Bars,.*?",
1105 | "category": 16
1106 | },
1107 | {
1108 | "url": "https://youtube.com/c/{}",
1109 | "domain": "youtube.com",
1110 | "err_text_pattern": "404\\sNot\\sFound.*?",
1111 | "category": 6
1112 | }
1113 | ]
1114 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiofiles==23.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0" \
2 | --hash=sha256:9312414ae06472eb6f1d163f555e466a23aed1c8f60c30cccf7121dba2e53eb2 \
3 | --hash=sha256:edd247df9a19e0db16534d4baaf536d6609a43e1de5401d7a4c1c148753a1635
4 | aiohttp-jinja2==1.5.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
5 | --hash=sha256:45cf00b80ab4dcc19515df13a929826eeb9698e76a3bcfd99112418751f5a061 \
6 | --hash=sha256:8d149b2a57d91f794b33a394ea5bc66b567f38c74a5a6a9477afc2450f105c01
7 | aiohttp==3.8.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
8 | --hash=sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14 \
9 | --hash=sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391 \
10 | --hash=sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2 \
11 | --hash=sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e \
12 | --hash=sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9 \
13 | --hash=sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd \
14 | --hash=sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4 \
15 | --hash=sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b \
16 | --hash=sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41 \
17 | --hash=sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567 \
18 | --hash=sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275 \
19 | --hash=sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54 \
20 | --hash=sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a \
21 | --hash=sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef \
22 | --hash=sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99 \
23 | --hash=sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da \
24 | --hash=sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4 \
25 | --hash=sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e \
26 | --hash=sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699 \
27 | --hash=sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04 \
28 | --hash=sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719 \
29 | --hash=sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131 \
30 | --hash=sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e \
31 | --hash=sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f \
32 | --hash=sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd \
33 | --hash=sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f \
34 | --hash=sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e \
35 | --hash=sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1 \
36 | --hash=sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed \
37 | --hash=sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4 \
38 | --hash=sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1 \
39 | --hash=sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777 \
40 | --hash=sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531 \
41 | --hash=sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b \
42 | --hash=sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab \
43 | --hash=sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8 \
44 | --hash=sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074 \
45 | --hash=sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc \
46 | --hash=sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643 \
47 | --hash=sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01 \
48 | --hash=sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36 \
49 | --hash=sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24 \
50 | --hash=sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654 \
51 | --hash=sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d \
52 | --hash=sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241 \
53 | --hash=sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51 \
54 | --hash=sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f \
55 | --hash=sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2 \
56 | --hash=sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15 \
57 | --hash=sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf \
58 | --hash=sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b \
59 | --hash=sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71 \
60 | --hash=sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05 \
61 | --hash=sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52 \
62 | --hash=sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3 \
63 | --hash=sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6 \
64 | --hash=sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a \
65 | --hash=sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519 \
66 | --hash=sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a \
67 | --hash=sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333 \
68 | --hash=sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6 \
69 | --hash=sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d \
70 | --hash=sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57 \
71 | --hash=sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c \
72 | --hash=sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9 \
73 | --hash=sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea \
74 | --hash=sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332 \
75 | --hash=sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5 \
76 | --hash=sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622 \
77 | --hash=sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71 \
78 | --hash=sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb \
79 | --hash=sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a \
80 | --hash=sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff \
81 | --hash=sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945 \
82 | --hash=sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480 \
83 | --hash=sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6 \
84 | --hash=sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9 \
85 | --hash=sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd \
86 | --hash=sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f \
87 | --hash=sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a \
88 | --hash=sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a \
89 | --hash=sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949 \
90 | --hash=sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc \
91 | --hash=sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75 \
92 | --hash=sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f \
93 | --hash=sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10 \
94 | --hash=sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f
95 | aiosignal==1.3.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
96 | --hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \
97 | --hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17
98 | appnope==0.1.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "darwin" \
99 | --hash=sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24 \
100 | --hash=sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e
101 | asttokens==2.2.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
102 | --hash=sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3 \
103 | --hash=sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c
104 | async-timeout==4.0.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
105 | --hash=sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15 \
106 | --hash=sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c
107 | attrs==22.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
108 | --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \
109 | --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99
110 | backcall==0.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
111 | --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
112 | --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
113 | charset-normalizer==3.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
114 | --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \
115 | --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \
116 | --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \
117 | --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \
118 | --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \
119 | --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \
120 | --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \
121 | --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \
122 | --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \
123 | --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \
124 | --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \
125 | --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \
126 | --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \
127 | --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \
128 | --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \
129 | --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \
130 | --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \
131 | --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \
132 | --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \
133 | --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \
134 | --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \
135 | --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \
136 | --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \
137 | --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \
138 | --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \
139 | --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \
140 | --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \
141 | --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \
142 | --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \
143 | --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \
144 | --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \
145 | --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \
146 | --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \
147 | --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \
148 | --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \
149 | --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \
150 | --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \
151 | --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \
152 | --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \
153 | --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \
154 | --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \
155 | --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \
156 | --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \
157 | --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \
158 | --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \
159 | --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \
160 | --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \
161 | --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \
162 | --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \
163 | --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \
164 | --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \
165 | --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \
166 | --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \
167 | --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \
168 | --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \
169 | --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \
170 | --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \
171 | --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \
172 | --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \
173 | --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \
174 | --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \
175 | --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \
176 | --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \
177 | --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \
178 | --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \
179 | --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \
180 | --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \
181 | --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \
182 | --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \
183 | --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \
184 | --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \
185 | --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \
186 | --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \
187 | --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \
188 | --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \
189 | --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \
190 | --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \
191 | --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \
192 | --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \
193 | --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \
194 | --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \
195 | --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \
196 | --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \
197 | --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \
198 | --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \
199 | --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \
200 | --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \
201 | --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8
202 | colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
203 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
204 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
205 | decorator==5.1.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
206 | --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
207 | --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
208 | executing==1.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
209 | --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \
210 | --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107
211 | frozenlist==1.3.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
212 | --hash=sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c \
213 | --hash=sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f \
214 | --hash=sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a \
215 | --hash=sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784 \
216 | --hash=sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27 \
217 | --hash=sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d \
218 | --hash=sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3 \
219 | --hash=sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678 \
220 | --hash=sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a \
221 | --hash=sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483 \
222 | --hash=sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8 \
223 | --hash=sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf \
224 | --hash=sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99 \
225 | --hash=sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c \
226 | --hash=sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48 \
227 | --hash=sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5 \
228 | --hash=sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56 \
229 | --hash=sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e \
230 | --hash=sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1 \
231 | --hash=sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401 \
232 | --hash=sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4 \
233 | --hash=sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e \
234 | --hash=sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649 \
235 | --hash=sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a \
236 | --hash=sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d \
237 | --hash=sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0 \
238 | --hash=sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6 \
239 | --hash=sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d \
240 | --hash=sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b \
241 | --hash=sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6 \
242 | --hash=sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf \
243 | --hash=sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef \
244 | --hash=sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7 \
245 | --hash=sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842 \
246 | --hash=sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba \
247 | --hash=sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420 \
248 | --hash=sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b \
249 | --hash=sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d \
250 | --hash=sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332 \
251 | --hash=sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936 \
252 | --hash=sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816 \
253 | --hash=sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91 \
254 | --hash=sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420 \
255 | --hash=sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448 \
256 | --hash=sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411 \
257 | --hash=sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4 \
258 | --hash=sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32 \
259 | --hash=sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b \
260 | --hash=sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0 \
261 | --hash=sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530 \
262 | --hash=sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669 \
263 | --hash=sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7 \
264 | --hash=sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1 \
265 | --hash=sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5 \
266 | --hash=sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce \
267 | --hash=sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4 \
268 | --hash=sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e \
269 | --hash=sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2 \
270 | --hash=sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d \
271 | --hash=sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9 \
272 | --hash=sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642 \
273 | --hash=sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0 \
274 | --hash=sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703 \
275 | --hash=sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb \
276 | --hash=sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1 \
277 | --hash=sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13 \
278 | --hash=sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab \
279 | --hash=sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38 \
280 | --hash=sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb \
281 | --hash=sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb \
282 | --hash=sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81 \
283 | --hash=sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8 \
284 | --hash=sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd \
285 | --hash=sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4
286 | idna==3.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
287 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
288 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
289 | ipython==8.10.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
290 | --hash=sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e \
291 | --hash=sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345
292 | jedi==0.18.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
293 | --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \
294 | --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612
295 | jinja2==3.1.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
296 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
297 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
298 | jsonpickle==3.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
299 | --hash=sha256:032538804795e73b94ead410800ac387fdb6de98f8882ac957fcd247e3a85200 \
300 | --hash=sha256:130d8b293ea0add3845de311aaba55e6d706d0bb17bc123bd2c8baf8a39ac77c
301 | markupsafe==2.1.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
302 | --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \
303 | --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \
304 | --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \
305 | --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \
306 | --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \
307 | --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \
308 | --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \
309 | --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \
310 | --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \
311 | --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \
312 | --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \
313 | --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \
314 | --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \
315 | --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \
316 | --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \
317 | --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \
318 | --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \
319 | --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \
320 | --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \
321 | --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \
322 | --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \
323 | --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \
324 | --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \
325 | --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \
326 | --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \
327 | --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \
328 | --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \
329 | --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \
330 | --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \
331 | --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \
332 | --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \
333 | --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \
334 | --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \
335 | --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \
336 | --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \
337 | --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \
338 | --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \
339 | --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \
340 | --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \
341 | --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \
342 | --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \
343 | --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \
344 | --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \
345 | --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \
346 | --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \
347 | --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \
348 | --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \
349 | --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \
350 | --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \
351 | --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58
352 | matplotlib-inline==0.1.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
353 | --hash=sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311 \
354 | --hash=sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304
355 | multidict==6.0.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
356 | --hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \
357 | --hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \
358 | --hash=sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03 \
359 | --hash=sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710 \
360 | --hash=sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161 \
361 | --hash=sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664 \
362 | --hash=sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569 \
363 | --hash=sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067 \
364 | --hash=sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313 \
365 | --hash=sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706 \
366 | --hash=sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2 \
367 | --hash=sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636 \
368 | --hash=sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49 \
369 | --hash=sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93 \
370 | --hash=sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603 \
371 | --hash=sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0 \
372 | --hash=sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60 \
373 | --hash=sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4 \
374 | --hash=sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e \
375 | --hash=sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1 \
376 | --hash=sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60 \
377 | --hash=sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951 \
378 | --hash=sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc \
379 | --hash=sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe \
380 | --hash=sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95 \
381 | --hash=sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d \
382 | --hash=sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8 \
383 | --hash=sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed \
384 | --hash=sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2 \
385 | --hash=sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775 \
386 | --hash=sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87 \
387 | --hash=sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c \
388 | --hash=sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2 \
389 | --hash=sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98 \
390 | --hash=sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3 \
391 | --hash=sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe \
392 | --hash=sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78 \
393 | --hash=sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660 \
394 | --hash=sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176 \
395 | --hash=sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e \
396 | --hash=sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988 \
397 | --hash=sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c \
398 | --hash=sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c \
399 | --hash=sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0 \
400 | --hash=sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449 \
401 | --hash=sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f \
402 | --hash=sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde \
403 | --hash=sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5 \
404 | --hash=sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d \
405 | --hash=sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac \
406 | --hash=sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a \
407 | --hash=sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9 \
408 | --hash=sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca \
409 | --hash=sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11 \
410 | --hash=sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35 \
411 | --hash=sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063 \
412 | --hash=sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b \
413 | --hash=sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982 \
414 | --hash=sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258 \
415 | --hash=sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1 \
416 | --hash=sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52 \
417 | --hash=sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480 \
418 | --hash=sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7 \
419 | --hash=sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461 \
420 | --hash=sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d \
421 | --hash=sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc \
422 | --hash=sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779 \
423 | --hash=sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a \
424 | --hash=sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547 \
425 | --hash=sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0 \
426 | --hash=sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171 \
427 | --hash=sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf \
428 | --hash=sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d \
429 | --hash=sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba
430 | networkx==3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
431 | --hash=sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e \
432 | --hash=sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412
433 | parso==0.8.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
434 | --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
435 | --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
436 | pexpect==4.8.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform != "win32" \
437 | --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
438 | --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
439 | pickleshare==0.7.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
440 | --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
441 | --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
442 | prompt-toolkit==3.0.37 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
443 | --hash=sha256:6a2948ec427dfcc7c983027b1044b355db6aaa8be374f54ad2015471f7d81c5b \
444 | --hash=sha256:d5d73d4b5eb1a92ba884a88962b157f49b71e06c4348b417dd622b25cdd3800b
445 | ptyprocess==0.7.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform != "win32" \
446 | --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
447 | --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
448 | pure-eval==0.2.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
449 | --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
450 | --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
451 | pygments==2.14.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
452 | --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \
453 | --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717
454 | pyvis==0.3.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
455 | --hash=sha256:aa817caac54afc9a4df38ada6f46636b842a8bd57197bb6aca5084699da04307
456 | six==1.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
457 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
458 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
459 | stack-data==0.6.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
460 | --hash=sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815 \
461 | --hash=sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8
462 | traitlets==5.9.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
463 | --hash=sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8 \
464 | --hash=sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9
465 | wcwidth==0.2.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
466 | --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
467 | --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
468 | yarl==1.8.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \
469 | --hash=sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87 \
470 | --hash=sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89 \
471 | --hash=sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a \
472 | --hash=sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08 \
473 | --hash=sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996 \
474 | --hash=sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077 \
475 | --hash=sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901 \
476 | --hash=sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e \
477 | --hash=sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee \
478 | --hash=sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574 \
479 | --hash=sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165 \
480 | --hash=sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634 \
481 | --hash=sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229 \
482 | --hash=sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b \
483 | --hash=sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f \
484 | --hash=sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7 \
485 | --hash=sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf \
486 | --hash=sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89 \
487 | --hash=sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0 \
488 | --hash=sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1 \
489 | --hash=sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe \
490 | --hash=sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf \
491 | --hash=sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76 \
492 | --hash=sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951 \
493 | --hash=sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863 \
494 | --hash=sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06 \
495 | --hash=sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562 \
496 | --hash=sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6 \
497 | --hash=sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c \
498 | --hash=sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e \
499 | --hash=sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1 \
500 | --hash=sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3 \
501 | --hash=sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3 \
502 | --hash=sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778 \
503 | --hash=sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8 \
504 | --hash=sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2 \
505 | --hash=sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b \
506 | --hash=sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d \
507 | --hash=sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f \
508 | --hash=sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c \
509 | --hash=sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581 \
510 | --hash=sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918 \
511 | --hash=sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c \
512 | --hash=sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e \
513 | --hash=sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220 \
514 | --hash=sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37 \
515 | --hash=sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739 \
516 | --hash=sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77 \
517 | --hash=sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6 \
518 | --hash=sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42 \
519 | --hash=sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946 \
520 | --hash=sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5 \
521 | --hash=sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d \
522 | --hash=sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146 \
523 | --hash=sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a \
524 | --hash=sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83 \
525 | --hash=sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef \
526 | --hash=sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80 \
527 | --hash=sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588 \
528 | --hash=sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5 \
529 | --hash=sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2 \
530 | --hash=sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef \
531 | --hash=sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826 \
532 | --hash=sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05 \
533 | --hash=sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516 \
534 | --hash=sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0 \
535 | --hash=sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4 \
536 | --hash=sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2 \
537 | --hash=sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0 \
538 | --hash=sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd \
539 | --hash=sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8 \
540 | --hash=sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b \
541 | --hash=sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1 \
542 | --hash=sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c
543 |
--------------------------------------------------------------------------------