├── .github ├── dependabot.yml └── workflows │ ├── pythonapp.yml │ └── pythonpublish.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── makefile ├── package-lock.json ├── package.json ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── screenshots ├── flask_1.png ├── flask_2.png ├── masonite_1.png ├── masonite_2.png └── terminal_handler.png ├── setup.cfg ├── setup.py ├── src ├── __init__.py └── exceptionite │ ├── Action.py │ ├── Block.py │ ├── Handler.py │ ├── StackTrace.py │ ├── Tab.py │ ├── __init__.py │ ├── assets │ ├── App.vue │ ├── Exceptionite.js │ ├── app.css │ ├── app.js │ ├── components │ │ ├── Alert.vue │ │ ├── AppButton.vue │ │ ├── Badge.vue │ │ ├── BaseActionDialog.vue │ │ ├── ContextMenu.vue │ │ ├── CopyButton.vue │ │ ├── Exception.vue │ │ ├── FirstSolution.vue │ │ ├── Frame.vue │ │ ├── FrameVars.vue │ │ ├── KeyValItem.vue │ │ ├── KeyValList.vue │ │ ├── Navbar.vue │ │ ├── NoSolution.vue │ │ ├── Pulse.vue │ │ ├── ShareDialog.vue │ │ ├── ShareSelector.vue │ │ ├── Sponsor.vue │ │ ├── Stack.vue │ │ ├── Switch.vue │ │ ├── blocks │ │ │ ├── Basic.vue │ │ │ ├── PackagesUpdates.vue │ │ │ ├── PossibleSolutions.vue │ │ │ ├── StackOverflow.vue │ │ │ └── index.js │ │ └── tabs │ │ │ ├── Basic.vue │ │ │ ├── Dumps.vue │ │ │ └── index.js │ ├── editorUrl.js │ └── themes │ │ ├── atom-one-dark.css │ │ └── atom-one-light.css │ ├── blocks │ ├── Environment.py │ ├── Git.py │ ├── Packages.py │ ├── PackagesUpdates.py │ ├── PossibleSolutions.py │ ├── StackOverflow.py │ └── __init__.py │ ├── django │ ├── ExceptioniteReporter.py │ ├── __init__.py │ ├── drf_exception_handler.py │ └── options.py │ ├── exceptions.py │ ├── flask │ ├── ExceptioniteReporter.py │ ├── __init__.py │ └── options.py │ ├── options.py │ ├── renderers │ ├── JSONRenderer.py │ ├── TerminalRenderer.py │ ├── WebRenderer.py │ └── __init__.py │ ├── solutions.py │ ├── tabs │ ├── ContextTab.py │ ├── RecommendationsTab.py │ ├── SolutionsTab.py │ └── __init__.py │ ├── templates │ ├── exception.html │ ├── exceptionite.css │ └── exceptionite.js │ └── version.py ├── tailwind.config.js ├── tests ├── test_handler.py ├── test_json_renderer.py ├── test_terminal_renderer.py └── test_web_renderer.py └── webpack.mix.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Maintain dependencies for cookiecutter repo 10 | - package-ecosystem: "pip" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | # Allow up to 10 open pull requests for pip dependencies 15 | open-pull-requests-limit: 10 16 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Test Application 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.7", "3.8", "3.9", "3.10"] 11 | name: Python ${{ matrix.python-version }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | make init 21 | - name: Test with pytest and Build coverage 22 | run: | 23 | make coverage 24 | - name: Upload coverage 25 | uses: codecov/codecov-action@v3 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | fail_ci_if_error: false 29 | 30 | lint: 31 | runs-on: ubuntu-latest 32 | name: Lint 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set up Python 3.8 36 | uses: actions/setup-python@v4 37 | with: 38 | python-version: 3.8 39 | - name: Intall Flake8 40 | run: | 41 | pip install flake8 42 | - name: Lint 43 | run: make lint 44 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.x" 16 | - name: Install dependencies 17 | run: | 18 | make init 19 | - name: Set up Node.js 14 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: "14.x" 23 | - name: Install Dependencies 24 | run: npm install --legacy-peer-deps 25 | - name: Compile frontend assets 26 | run: | 27 | npm run production 28 | # here we run tests after having compiled the assets, because tests can use it 29 | - name: Publish only packages passing test 30 | run: | 31 | make test 32 | # we need to use --allow-empty when compiled assets don't change from one version to the next 33 | - name: Bump version with compiled assets 34 | shell: bash 35 | env: 36 | RELEASE_VERSION: ${{ github.event.release.tag_name }} 37 | run: | 38 | git config --local user.email "action@github.com" 39 | git config --local user.name "GitHub Action" 40 | git add -f src/exceptionite/templates/exceptionite.js 41 | git add -f src/exceptionite/templates/exceptionite.css 42 | git commit -m"Release $RELEASE_VERSION" --allow-empty 43 | - name: Push compiled assets 44 | uses: ad-m/github-push-action@master 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | branch: ${{ github.event.release.target_commitish }} 48 | # override the tag created by GitHub release, now this tag will contains the compiled assets 49 | tags: true 50 | force: true 51 | - name: Publish package 52 | env: 53 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 54 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 55 | run: | 56 | make publish 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | **/__pycache__/* 3 | .vscode 4 | package.db 5 | .env 6 | dist/* 7 | src/exceptionite.egg-info 8 | src/exceptionite/templates/*.js 9 | src/exceptionite/templates/*.css 10 | build/ 11 | node_modules/ 12 | **/mix-manifest.json -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting up this repository for development 2 | 3 | To setup the package to get your package up and running, you should first take a look at `setup.py` and make any packages specific changes there. These include the classifiers and package name. 4 | 5 | Then you should create a virtual environment and activate it 6 | 7 | ``` 8 | python3 -m venv venv 9 | source venv/bin/activate 10 | ``` 11 | 12 | Then install from the requirements file 13 | 14 | ``` 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | Finally you need to compile install frontend dependencies and compile assets: 19 | 20 | ``` 21 | npm install 22 | npm run dev 23 | ``` 24 | 25 | Note that in development, you can compile assets at each change with: 26 | 27 | ``` 28 | npm run watch 29 | ``` 30 | 31 | This will install `exceptionite` and development related packages. 32 | 33 | ## Running tests 34 | 35 | ``` 36 | python -m pytest 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Mancuso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/exceptionite/templates/* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Exceptionite 3 | 4 |

5 | 6 | Masonite Package 7 | 8 | GitHub Workflow Status 9 | Python Version 10 | PyPI 11 | License 12 | Code style: black 13 |

14 | 15 | A Python exception library designed to make handling and displaying exceptions a cinch. 16 | 17 | Exceptions can be rendered into a beautiful HTML exception page! 18 | 19 | ![](screenshots/masonite_1.png) 20 | ![](screenshots/masonite_2.png) 21 | 22 | or in your terminal: 23 | 24 | ![](screenshots/terminal_handler.png) 25 | 26 | 27 | # Getting Started 28 | 29 | First install the package: 30 | 31 | ```bash 32 | pip install exceptionite 33 | ``` 34 | 35 | Then you can follow instruction for your use case: 36 | 37 | - [Masonite](#usage-for-masonite) 38 | - [Flask](#usage-for-flask) 39 | - [Django](#usage-for-django) 40 | - [Django REST framework](#usage-for-django-rest-framework) 41 | - [Pyramid](#usage-for-pyramid) 42 | - [Basic Python](#usage-for-python) 43 | 44 | ## Usage for Masonite 45 | 46 | Masonite 4 is already using `exceptionite` for its default error page so you don't have anything 47 | to set up. 48 | If you are using `Masonite < 4.0`, please use `exceptionite < 2.0`. 49 | 50 | ## Usage for Flask 51 | 52 | If you are using `Flask` you can also use this package! Here is an example for a flask application: 53 | 54 | ```python 55 | from flask import Flask, request 56 | from exceptionite.flask import ExceptioniteReporter 57 | 58 | app = Flask(__name__) 59 | 60 | 61 | @app.errorhandler(Exception) 62 | def handle_exception(exception): 63 | handler = ExceptioniteReporter(exception, request) 64 | # display exception stack trace nicely in console 65 | handler.terminal() 66 | return handler.html() 67 | 68 | 69 | @app.route("/") 70 | def hello(world): 71 | test = "Hello World" 72 | return 2 / 0 73 | 74 | 75 | if __name__ == "__main__": 76 | app.run(debug=True) 77 | ``` 78 | 79 | You'll now see this beautiful exception page: 80 | ![](screenshots/flask_1.png) 81 | ![](screenshots/flask_2.png) 82 | 83 | 84 | ## Usage for Django 85 | 86 | You can customize error reports in Django in `DEBUG` mode as explained in the [docs](https://docs.djangoproject.com/en/3.2/howto/error-reporting/#custom-error-reports). 87 | 88 | Install the package if you haven't done so yet 89 | 90 | ```python 91 | # settings.py 92 | $ pip install exceptionite 93 | ``` 94 | 95 | Then simple set a default exception reporter to the exceptionite one. Be careful this reporter 96 | should only be used for local development with `DEBUG=True`: 97 | 98 | ```python 99 | # myapp/settings.py 100 | if DEBUG: 101 | DEFAULT_EXCEPTION_REPORTER = "exceptionite.django.ExceptioniteReporter" 102 | ``` 103 | 104 | If you want Django 404 to be also handled by exceptionite you should add an other reporter: 105 | 106 | ```python 107 | # myapp/settings.py 108 | if DEBUG: 109 | # handle 404 errors 110 | from exceptionite.django import Exceptionite404Reporter 111 | 112 | Exceptionite404Reporter() 113 | 114 | # handle all other errors 115 | DEFAULT_EXCEPTION_REPORTER = "exceptionite.django.ExceptioniteReporter" 116 | ``` 117 | 118 | ## Usage for Django REST framework 119 | 120 | You can also customize error reports when using Django REST framework package in `DEBUG` mode as explained in the [docs](https://www.django-rest-framework.org/api-guide/exceptions/). 121 | 122 | Install the package if you haven't done so yet 123 | 124 | ```python 125 | # settings.py 126 | $ pip install exceptionite 127 | ``` 128 | 129 | Then simple set the REST default exception reporter to the exceptionite one: 130 | 131 | ```python 132 | # myapp/settings.py 133 | if DEBUG: 134 | REST_FRAMEWORK = { 135 | "EXCEPTION_HANDLER": "exceptionite.django.drf_exception_handler" 136 | } 137 | ``` 138 | 139 | Now when doing API requests accepting `application/json` a JSON debug error page 140 | will be returned. When using the Django REST framework browsable API or accessing a GET endpoint from your browser (`text/html`) the HTML exceptionite page will be 141 | displayed ! 142 | 143 | :warning: Note that this handler will change exception handling behaviour and should be only used when DEBUG mode is enabled. 144 | 145 | If you want to customize exception handling for other cases you can do: 146 | 147 | ```python 148 | # app/exception_handler.py 149 | from exceptionite.django import drf_exception_handler 150 | 151 | def custom_handler(exc, context): 152 | # do what you want here 153 | 154 | response = drf_exception_handler(exc, context) 155 | 156 | # do what you want here 157 | return response 158 | ``` 159 | 160 | ```python 161 | # myapp/settings.py 162 | 163 | REST_FRAMEWORK = { 164 | "EXCEPTION_HANDLER": "myapp.exception_handler.custom_handler" 165 | } 166 | ``` 167 | 168 | ## Usage for Pyramid 169 | 170 | If you are using `Pyramid` you can also use this package! You just need to register two handlers 171 | function to handle 404 and any other errors. 172 | 173 | Here is an example for a simple pyramid application: 174 | 175 | ```python 176 | from wsgiref.simple_server import make_server 177 | from pyramid.config import Configurator 178 | from pyramid.response import Response 179 | from pyramid.view import exception_view_config, notfound_view_config 180 | 181 | from exceptionite import Handler 182 | 183 | handler = Handler() 184 | 185 | 186 | @exception_view_config(Exception) 187 | def handle_all_exceptions(exc, request): 188 | handler.start(exc) 189 | handler.render("terminal") 190 | response = Response(handler.render("web")) 191 | response.status_int = 500 192 | return response 193 | 194 | 195 | @notfound_view_config() 196 | def handle_404(exc, request): 197 | handler.start(exc) 198 | handler.render("terminal") 199 | response = Response(handler.render("web")) 200 | response.status_int = 404 201 | return response 202 | 203 | def hello_world(request): 204 | 1 / 0 205 | return Response("Hello World!") 206 | 207 | 208 | if __name__ == "__main__": 209 | with Configurator() as config: 210 | config.add_route("hello", "/") 211 | config.add_view(hello_world, route_name="hello") 212 | # this line below is very important to scan our decorated error handlers 213 | config.scan() 214 | app = config.make_wsgi_app() 215 | server = make_server("127.0.0.1", 8000, app) 216 | server.serve_forever() 217 | ``` 218 | 219 | ## Usage for Python 220 | 221 | If you are not using a specific framework you can still use this library. You just have to get 222 | an instance of the `Handler` class and use the `start()` method to start handling the exception. 223 | 224 | Then you can get useful information easily and also define how you want to render the error. You 225 | can even add your own renderer. 226 | 227 | ```python 228 | from exceptionite import Handler 229 | 230 | try: 231 | 2/0 232 | except Exception as e: 233 | handler = Handler() 234 | handler.start(e) 235 | ``` 236 | 237 | Once you have the handler class theres a whole bunch of things we can now do! 238 | 239 | ### Getting Exception Details 240 | 241 | Getting the exception name: 242 | 243 | ```python 244 | handler.exception() #== ZeroDivisionError 245 | ``` 246 | 247 | Getting the exception message: 248 | 249 | ```python 250 | handler.message() #== cannot divide by 0 251 | ``` 252 | 253 | Getting the exception namespace: 254 | 255 | ```python 256 | handler.namespace() #== builtins.ZeroDivisionError 257 | ``` 258 | 259 | ### Rendering an HTML page 260 | 261 | You can render an elegant exception page by using the `render` method with the [WebRenderer](#renderers): 262 | 263 | ```python 264 | handler.render("web") #== ... 265 | ``` 266 | 267 | If you have a framework or an application you can swap the exception handler out with this handler 268 | and then call this method. 269 | 270 | ### Adding Context 271 | 272 | Sometimes you will need to add more information to the exception page. This is where contexts come into play. Contexts are the ability to add any information you need to help you debug information. 273 | 274 | If you use a framework like Masonite you might want to see information related to your Service Providers. If you use a framework like django you might want to see a list of your installed apps. 275 | 276 | On the left side of the page below the stack trace you will find the context menu. Context is organised into blocks. 277 | 278 | 1. You can register new contexts quickly by supplying a dictionary or a callable providing a dictionary: 279 | 280 | ```python 281 | import sys 282 | 283 | handler.renderer("web").context("System Variables", {"sys argv": sys.argv}) 284 | ``` 285 | 286 | 2. Or you [can create custom blocks](#block) and add them to the `Context` tab: 287 | 288 | ```python 289 | import sys 290 | from exceptionite import Block 291 | 292 | class SystemVarsBlock(Block): 293 | id = "system_vars" 294 | name= "System Variables" 295 | 296 | def build(self): 297 | return { 298 | "sys argv": sys.argv 299 | } 300 | 301 | handler.renderer("web").tab("context").add_blocks(SystemVarsBlock) 302 | ``` 303 | 304 | The second method allows to customize the block. 305 | 306 | ### Hiding Sensitive Data 307 | 308 | Exceptionite is configured by default to scrub data displayed in tab and blocks. This allows hiding sensitive data, by replacing it with `*****`. 309 | 310 | Hiding sensitive data can be disabled globally in handler options with `options.hide_sensitive_data`. 311 | 312 | It can also be disabled per block by setting `disable_scrubbing = True` in [block class](#block). 313 | 314 | The keywords defined to find sensitive data can be edited: 315 | 316 | ```python 317 | # define a new set of keywords 318 | handler.set_scrub_keywords(["app_secret", "pwd"]) 319 | # add new keywords 320 | handler.add_scrub_keywords(["app_secret", "pwd"]) 321 | ``` 322 | 323 | You can see the default keywords by accessing `handler.scrub_keywords`. 324 | 325 | 326 | # Configuration 327 | 328 | ## Options 329 | 330 | The `exceptionite` handler comes with the default options below: 331 | ```python 332 | { 333 | "options": { 334 | "editor": "vscode", 335 | "search_url": "https://www.google.com/search?q=", 336 | "links": { 337 | "doc": "https://docs.masoniteproject.com", 338 | "repo": "https://github.com/MasoniteFramework/masonite", 339 | }, 340 | "stack": {"offset": 8, "shorten": True}, 341 | "hide_sensitive_data": True 342 | }, 343 | "handlers": { 344 | "context": True, 345 | "solutions": {"stackoverflow": False, "possible_solutions": True}, 346 | "recommendations": {"packages_updates": {"list": ["exceptionite"]}}, 347 | }, 348 | } 349 | ``` 350 | 351 | You can configure it by using `set_options(options)` method: 352 | ```python 353 | from exceptionite import Handler, DefaultOptions 354 | 355 | handler = Handler() 356 | options = DefaultOptions 357 | options.get("options").update({"search_url": "https://duckduckgo.com/?q="}) 358 | handler.set_options(options) 359 | ``` 360 | 361 | For Masonite, options are defined in `exceptions.py` configuration file. 362 | 363 | ## Renderers 364 | 365 | When an error is caught by Exceptionite, it can be rendered in many ways through configured renderers. Available renderers are: 366 | - WebRenderer: renders the exception as a beautiful HTML error page 367 | - TerminalRenderer: renders the exception nicely in the console 368 | - JSONRenderer: renders the exception as a JSON payload (useful for API errors handling) 369 | 370 | A renderer is a simple Python class with `build()` and `render()` methods. Feel free to create 371 | your own one if needed ! 372 | ```python 373 | class CustomRenderer: 374 | def __init__(self, handler: "Handler"): 375 | self.handler = handler 376 | self.data = None 377 | 378 | def build(self): 379 | exception = self.handler.exception() 380 | stack = self.handler.stacktrace() 381 | # build data from exception here... 382 | data = ... 383 | return data 384 | 385 | def render(self) -> str: 386 | self.data = self.build() 387 | # render the data as you want 388 | return 389 | ``` 390 | 391 | To add a renderer: 392 | ```python 393 | handler.add_renderer("json", JSONRenderer) 394 | ``` 395 | 396 | Then to render the exception using the renderer: 397 | ```python 398 | handler.render("json") #== [{"exception": ...}] 399 | ``` 400 | 401 | 402 | ## Web Renderer Specific Options 403 | 404 | The HTML exception page created with the WebRenderer is highly configurable: 405 | 406 | - Search engine used to search the error message on the web: 407 | ```python 408 | "options": { 409 | "search_url": "https://www.google.com/search?q=" 410 | # ... 411 | } 412 | ``` 413 | - Editor used to open the line on the stack trace at click: 414 | 415 | ```python 416 | "options": { 417 | "editor": "sublime" 418 | # ... 419 | } 420 | ``` 421 | 422 | Available editors are `vscode`, `pycharm`, `idea`, `sublime`, `atom`, `textmate`, `emacs`, `macvim`, `vscodium`. 423 | 424 | - Navbar links to documentation and repository can be customized: 425 | 426 | ```python 427 | "options": { 428 | "links": { 429 | "doc": "https://docs.masoniteproject.com", 430 | "repo": "https://github.com/MasoniteFramework/exceptionite", 431 | }, 432 | } 433 | ``` 434 | 435 | - Tabs can be disabled. To disable `solutions` tab: 436 | ```python 437 | "handlers": { 438 | "solutions": False 439 | } 440 | ``` 441 | 442 | - New tabs can be added: 443 | ```python 444 | handler.renderer("web").add_tabs(DumpsTab) 445 | ``` 446 | 447 | 448 | ### Blocks and Tabs API 449 | 450 | #### Tab 451 | 452 | A tab is defined with the following attributes: 453 | - `id (str)`: slug of the tab that should be unique, and used to access it from handler 454 | - `name (str)`: name of the tab displayed in the error page 455 | - `icon (str)`: icon of the tab displayed in the error page (available icons are defined [here](https://heroicons.com/). The name should be converted to UpperCamelCase.) 456 | - `advertise_content (bool)`: if `True` a animated dot will be displayed in error page navbar, if `has_content()` method returns `True` 457 | - `disable_scrubbing (bool)`: if `True`, data won't be scrubbed, meaning that [sensitive data](#hiding-sensitive-data) won't be hidden. 458 | 459 | and with the following methods (that can be overriden): 460 | - `has_content()`: should returns a `bool` that indicates if tab has content. 461 | 462 | 463 | #### Block 464 | 465 | A block is located in a tab. A tab can have many blocks. Blocks are added to tab in the following manner: 466 | ```python 467 | class ContextTab(Tab): 468 | 469 | name = "Context" 470 | id = "context" 471 | icon = "ViewListIcon" 472 | 473 | def __init__(self, handler): 474 | super().__init__(handler) 475 | self.add_blocks(Environment, Packages, Git) 476 | # OR 477 | handler.renderer("web").tab("context").add_blocks(Environment, Packages, Git) 478 | ``` 479 | 480 | A block is defined with the following attributes: 481 | - `id (str)`: slug of the block that should be unique, and used to access it from handler 482 | - `name (str)`: name of the block displayed in the error page 483 | - `icon (str)`: icon of the block displayed in the error page (available icons are defined [here](https://heroicons.com/). The name should be converted to UpperCamelCase.) 484 | - `disable_scrubbing (bool)`: if `True`, data won't be scrubbed, meaning that [sensitive data](#hiding-sensitive-data) won't be hidden. 485 | - `empty_msg (str)`: message that will be displayed if block data is empty. 486 | - `has_sections (bool)`: enable this to render dict as sections, where each root key is a section title and value is a nested dict. 487 | 488 | and with the following methods: 489 | 490 | - `build()`: must be implemented and should returns data that can be serialized. This data will be provided to the Vue block component. 491 | - `has_content()`: should returns a `bool` that indicates if tab has content. 492 | 493 | 494 | # Contributing 495 | 496 | Please read the [Contributing Documentation](CONTRIBUTING.md) here. 497 | 498 | # Maintainers 499 | 500 | - [Joseph Mancuso](https://github.com/josephmancuso) 501 | - [Samuel Girardin](https://www.github.com/girardinsamuel) 502 | 503 | # License 504 | 505 | Exceptionite is open-sourced software licensed under the [MIT license](LICENSE). 506 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: ## Show this help 3 | @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 4 | 5 | init: ## Install package dependencies 6 | pip install --upgrade pip 7 | # install package and dev dependencies (see setup.py) 8 | pip install '.[test]' 9 | test: ## Run package tests 10 | python -m pytest tests 11 | ci: ## [CI] Run package tests and lint 12 | make test 13 | make lint 14 | lint: ## Run code linting 15 | python -m flake8 src/exceptionite --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 16 | format: ## Format code with Black 17 | black src/exceptionite 18 | coverage: ## Run package tests and upload coverage reports 19 | python -m pytest --cov-report term --cov-report xml --cov=src/masonite/exceptionite tests 20 | publish: ## Publish package to pypi 21 | python setup.py sdist bdist_wheel 22 | twine upload dist/* 23 | rm -fr build dist .egg src/exceptionite.egg-info 24 | pypirc: ## Copy the template .pypirc in the repo to your home directory 25 | cp .pypirc ~/.pypirc -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exceptionite", 3 | "version": "2.0.0", 4 | "description": "", 5 | "scripts": { 6 | "dev": "mix", 7 | "watch": "mix watch", 8 | "watch-poll": "mix watch -- --watch-options-poll=1000", 9 | "hot": "mix watch --hot", 10 | "production": "mix --production" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@heroicons/vue": "^1.0.5", 16 | "@vue/compiler-sfc": "^3.2.29", 17 | "autoprefixer": "^10.4.2", 18 | "laravel-mix": "^6.0.43", 19 | "postcss": "^8.4.5", 20 | "tailwindcss": "^3.0.16", 21 | "vue": "^3.2.29", 22 | "vue-loader": "^16.8.3" 23 | }, 24 | "dependencies": { 25 | "@headlessui/vue": "^1.4.3", 26 | "@vueuse/core": "^7.5.5", 27 | "axios": "^0.25.0", 28 | "highlight.js": "^11.4.0", 29 | "tippy.js": "^6.3.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 99 3 | target-version = ['py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | \.github 9 | \.vscode 10 | | \.venv 11 | | docs 12 | | node_modules 13 | | build 14 | | src/exceptionite/templates 15 | | src/exceptionite/assets 16 | )/ 17 | ''' 18 | 19 | [tool.isort] 20 | profile = "black" 21 | multi_line_output = 3 22 | include_trailing_comma = true -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | -------------------------------------------------------------------------------- /screenshots/flask_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/screenshots/flask_1.png -------------------------------------------------------------------------------- /screenshots/flask_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/screenshots/flask_2.png -------------------------------------------------------------------------------- /screenshots/masonite_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/screenshots/masonite_1.png -------------------------------------------------------------------------------- /screenshots/masonite_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/screenshots/masonite_2.png -------------------------------------------------------------------------------- /screenshots/terminal_handler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/screenshots/terminal_handler.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | .github, 5 | .vscode, 6 | __pycache__, 7 | templates, 8 | node_modules, 9 | build, 10 | src/exceptionite/templates, 11 | src/exceptionite/assets, 12 | venv 13 | max-complexity = 10 14 | max-line-length = 99 15 | 16 | omit = 17 | */config/* 18 | setup.py 19 | */stubs/* 20 | wsgi.py 21 | tests/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | meta = {} 5 | with open( 6 | os.path.join(os.path.abspath(os.path.dirname(__file__)), "src", "exceptionite", "version.py"), 7 | "r", 8 | ) as f: 9 | exec(f.read(), meta) 10 | 11 | with open("README.md", "r") as fh: 12 | long_description = fh.read() 13 | 14 | setup( 15 | name="exceptionite", 16 | # Versions should comply with PEP440. For a discussion on single-sourcing 17 | # the version across setup.py and the project code, see 18 | # https://packaging.python.org/en/latest/single_source_version.html 19 | version=meta["__version__"], 20 | description="Exception Handling Made Easy", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | include_package_data=True, 24 | # The project's main homepage. 25 | url="https://github.com/masoniteframework/exceptionite", 26 | # Author details 27 | author="The Masonite Community", 28 | author_email="joe@masoniteproject.com", 29 | # Choose your license 30 | license="MIT license", 31 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 32 | classifiers=[ 33 | # How mature is this project? Common values are 34 | # 3 - Alpha 35 | # 4 - Beta 36 | # 5 - Production/Stable 37 | "Development Status :: 5 - Production/Stable", 38 | # Indicate who your project is intended for 39 | "Intended Audience :: Developers", 40 | "Topic :: Software Development :: Build Tools", 41 | "Environment :: Web Environment", 42 | # Pick your license as you wish (should match "license" above) 43 | "License :: OSI Approved :: MIT License", 44 | "Operating System :: OS Independent", 45 | # Specify the Python versions you support here. In particular, ensure 46 | # that you indicate whether you support Python 2, Python 3 or both. 47 | "Programming Language :: Python :: 3.4", 48 | "Programming Language :: Python :: 3.5", 49 | "Programming Language :: Python :: 3.6", 50 | "Programming Language :: Python :: 3.7", 51 | "Topic :: Internet :: WWW/HTTP", 52 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 53 | "Topic :: Internet :: WWW/HTTP :: WSGI", 54 | "Topic :: Software Development :: Libraries :: Application Frameworks", 55 | "Topic :: Software Development :: Libraries :: Python Modules", 56 | # List package on masonite packages website 57 | "Framework :: Masonite", 58 | ], 59 | # What does your project relate to? 60 | keywords="Masonite, Python, Development, Framework", 61 | # You can just specify the packages manually here if your project is 62 | # simple. Or you can use find_packages(). 63 | package_dir={"": "src"}, 64 | packages=[ 65 | "exceptionite", 66 | "exceptionite.blocks", 67 | "exceptionite.renderers", 68 | "exceptionite.tabs", 69 | "exceptionite.django", 70 | "exceptionite.flask", 71 | ], 72 | # List run-time dependencies here. These will be installed by pip when 73 | # your project is installed. For an analysis of "install_requires" vs pip's 74 | # requirements files see: 75 | # https://packaging.python.org/en/latest/requirements.html 76 | install_requires=["jinja2", "requests", "colorama", "dotty-dict", "typing-extensions", "mock"], 77 | # List additional groups of dependencies here (e.g. development 78 | # dependencies). You can install these using the following syntax, 79 | # for example: 80 | # $ pip install -e .[test] 81 | extras_require={ 82 | "test": [ 83 | "black", 84 | "flake8", 85 | "coverage", 86 | "pytest", 87 | "pytest-cov", 88 | "twine>=1.5.0", 89 | "wheel", 90 | ], 91 | }, 92 | ) 93 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/src/__init__.py -------------------------------------------------------------------------------- /src/exceptionite/Action.py: -------------------------------------------------------------------------------- 1 | class Action: 2 | name = "Action Name" 3 | icon = "" 4 | id = "" 5 | component = "" 6 | 7 | def __init__(self, handler) -> None: 8 | self.handler = handler 9 | 10 | def run(self, options={}): 11 | # do something 12 | return "Action executed !" 13 | 14 | def serialize(self): 15 | return { 16 | "id": self.id, 17 | "name": self.name, 18 | "icon": self.icon, 19 | "component": self.component, 20 | } 21 | -------------------------------------------------------------------------------- /src/exceptionite/Block.py: -------------------------------------------------------------------------------- 1 | class Block: 2 | id = "" 3 | name = "Block Name" 4 | icon = "" 5 | component = "BasicBlock" 6 | disable_scrubbing = False 7 | empty_msg = "No content." 8 | has_sections = False 9 | 10 | def __init__(self, tab, handler, options={}): 11 | # tab which holds the block 12 | self.tab = tab 13 | self.handler = handler 14 | self.options = options 15 | self.data = {} 16 | assert self.id, "Block should declare an 'id' attribute !" 17 | 18 | def serialize(self): 19 | raw_data = self.build() 20 | self.data = self.handler.scrub_data(raw_data, self.disable_scrubbing) 21 | return { 22 | "id": self.id, 23 | "name": self.name, 24 | "icon": self.icon, 25 | "component": self.component, 26 | "data": self.data, 27 | "empty_msg": self.empty_msg, 28 | "has_content": self.has_content(), 29 | "has_sections": self.has_sections, 30 | } 31 | 32 | def build(self): 33 | raise NotImplementedError("block should implement build()") 34 | 35 | def has_content(self): 36 | return True 37 | -------------------------------------------------------------------------------- /src/exceptionite/Handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from dotty_dict import dotty 4 | from typing import Type, TYPE_CHECKING 5 | from typing_extensions import Protocol 6 | 7 | if TYPE_CHECKING: 8 | 9 | class Renderer(Protocol): 10 | handler: "Handler" 11 | 12 | def __init__(self, handler: "Handler") -> None: 13 | ... 14 | 15 | def render(self) -> str: 16 | """Render exception with the given renderer""" 17 | ... 18 | 19 | 20 | from .StackTrace import StackTrace 21 | from .renderers import WebRenderer, TerminalRenderer, JSONRenderer 22 | from .options import DEFAULT_OPTIONS as DefaultOptions 23 | 24 | 25 | class Handler: 26 | """Exceptionite handler used to handle exceptions and render them using the given renderer.""" 27 | 28 | scrub_keywords = [ 29 | "password", 30 | "passwd", 31 | "pwd", 32 | "secret", 33 | "key", 34 | "api_key", 35 | "apikey", 36 | "access_token", 37 | "credentials", 38 | "token", 39 | ] 40 | 41 | def __init__( 42 | self, 43 | ): 44 | self.renderers: dict["Renderer"] = {} 45 | self.options: dict = dotty(DefaultOptions) 46 | self.context = {} 47 | self.add_renderer("web", WebRenderer) 48 | self.add_renderer("terminal", TerminalRenderer) 49 | self.add_renderer("json", JSONRenderer) 50 | 51 | def set_options(self, options: dict) -> "Handler": 52 | """Configure exceptionite handler with given options.""" 53 | # ensure options is a dict here, might already be a dotty dict 54 | self.options = dotty(dict(options)) 55 | return self 56 | 57 | def add_renderer(self, name: str, renderer_class: Type["Renderer"]) -> "Handler": 58 | """Register a renderer to handle exceptions.""" 59 | self.renderers.update({name: renderer_class(self)}) 60 | return self 61 | 62 | def renderer(self, name: str) -> "Renderer": 63 | """Get the renderer with the given name.""" 64 | return self.renderers[name] 65 | 66 | def add_context(self, name: str, data: dict) -> "Handler": 67 | self.context.update({name: data}) 68 | return self 69 | 70 | def start(self, exception: BaseException) -> "Handler": 71 | """Start handling the given exception.""" 72 | self._exception = exception 73 | self._type, self._value, self._original_traceback = sys.exc_info() 74 | traceback_exc = traceback.TracebackException( 75 | self._type, self._value, self._original_traceback, capture_locals=True 76 | ) 77 | self._stacktrace = StackTrace( 78 | traceback_exc, 79 | self._exception, 80 | offset=self.options.get("options.stack.offset"), 81 | shorten=self.options.get("options.stack.shorten"), 82 | scrubber=self.scrub_data, 83 | ) 84 | self._stacktrace.generate().reverse() 85 | return self 86 | 87 | # helpers 88 | def exception(self) -> str: 89 | """Get the handled exception name.""" 90 | return self._exception.__class__.__name__ 91 | 92 | def namespace(self) -> str: 93 | """Get the handled exception full namespace.""" 94 | return self._exception.__class__.__module__ + "." + self.exception() 95 | 96 | def message(self) -> str: 97 | """Get the handled exception message.""" 98 | return str(self._exception) 99 | 100 | def stacktrace(self) -> "StackTrace": 101 | """Get the handled exception stack trace object.""" 102 | return self._stacktrace 103 | 104 | def count(self): 105 | return len(self._stacktrace) 106 | 107 | def render(self, renderer: str) -> str: 108 | """Render the handled exception with the given renderer.""" 109 | return self.renderer(renderer).render() 110 | 111 | def add_scrub_keywords(self, keywords: list) -> "Handler": 112 | """Add new scrub keywords used to hide sensitive data.""" 113 | self.scrub_keywords.extend(keywords) 114 | # ensure keywords are not duplicated 115 | self.scrub_keywords = list(set(self.scrub_keywords)) 116 | return self 117 | 118 | def set_scrub_keywords(self, keywords: list) -> "Handler": 119 | """Override scrub keywords used to hide sensitive data.""" 120 | self.scrub_keywords = keywords 121 | return self 122 | 123 | def scrub_data(self, data: dict, disable: bool = False) -> dict: 124 | """Hide sensitive data of the given dictionary if enabled in the options with 125 | 'hide_sensitive_data' parameter.""" 126 | if not self.options.get("options.hide_sensitive_data") or disable: 127 | return data 128 | scrubbed_data = {} 129 | if not data: 130 | return scrubbed_data 131 | for key, val in data.items(): 132 | if not val: 133 | scrubbed_data[key] = val 134 | continue 135 | if isinstance(val, dict): 136 | scrubbed_data[key] = self.scrub_data(val, disable) 137 | else: 138 | # scrub entire value if key matches 139 | should_scrub = False 140 | for token in self.scrub_keywords: 141 | if token.lower() in key.lower(): 142 | should_scrub = True 143 | if should_scrub: 144 | scrubbed_val = "*****" 145 | else: 146 | scrubbed_val = val 147 | scrubbed_data[key] = scrubbed_val 148 | return scrubbed_data 149 | -------------------------------------------------------------------------------- /src/exceptionite/StackTrace.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | import pprint 4 | import os 5 | from typing import List 6 | import sys 7 | 8 | 9 | class StackFrame: 10 | """Model a frame in the stack trace.""" 11 | 12 | def __init__(self, index, frame_summary, variables={}, offset=5, shorten=False): 13 | self.index = index 14 | self.file = frame_summary[0] 15 | self.relative_file = None 16 | self.is_vendor = False 17 | 18 | # frame filename string should be shorten 19 | if shorten: 20 | rel_path = self.file.replace(f"{os.getcwd()}/", "") 21 | 22 | # check if frame is a vendor frame (from an external python package or masonite package 23 | # in development) 24 | if rel_path.startswith(sys.base_prefix) or rel_path.startswith(sys.exec_prefix): 25 | self.is_vendor = True 26 | self.relative_file = "~/" + rel_path.lstrip(sys.base_prefix) 27 | elif rel_path.find("src/masonite/") != -1: 28 | self.is_vendor = True 29 | cut_index = rel_path.find("src/masonite/") + len("src/") 30 | self.relative_file = "~/" + rel_path[cut_index:] 31 | else: # it's located in project 32 | self.relative_file = rel_path 33 | 34 | self.language = self.get_language(self.file) 35 | 36 | self.offset = offset 37 | self.lineno = frame_summary[1] 38 | self.offending_line = self.lineno 39 | self.parent_statement = frame_summary[2] 40 | self.statement = frame_summary[3] 41 | self.start_line = self.lineno - offset if (self.lineno - offset > 0) else 0 42 | self.end_line = self.lineno + offset 43 | self.variables = variables 44 | self.method = frame_summary.name if frame_summary.name != "" else "__main__" 45 | 46 | with open(self.file, "r", encoding="utf-8") as fp: 47 | printer = pprint.PrettyPrinter(indent=4) 48 | 49 | self.file_contents = printer.pformat(fp.read()).split("\\n")[ 50 | self.start_line : self.end_line # noqa: E203 51 | ] 52 | 53 | formatted_contents = {} 54 | read_line = self.start_line + 1 55 | 56 | for content in self.file_contents: 57 | if self.language == "python": 58 | formatted_line = ( 59 | content.replace("'\n '", "") 60 | .replace("'\n \"", "") 61 | .replace("\"\n '", "") 62 | .replace('"\n "', "") 63 | .replace("""(\'""", "") 64 | .replace("')", "") 65 | ) 66 | else: 67 | formatted_line = content.replace("'\n '", "").replace("'\n \"", "") 68 | 69 | if self.statement in formatted_line: 70 | self.offending_line = read_line 71 | 72 | formatted_contents.update({read_line: formatted_line}) 73 | read_line += 1 74 | # remove common higher indentation 75 | min_start_spaces = 1000 76 | for line in formatted_contents.values(): 77 | if line != "": 78 | min_start_spaces = min(min_start_spaces, len(line) - len(line.lstrip(" "))) 79 | 80 | self.file_contents = { 81 | number: text[min_start_spaces:] for number, text in formatted_contents.items() 82 | } 83 | 84 | def get_language(self, file: str) -> str: 85 | """Resolve language from the file path.""" 86 | if file.endswith(".py"): 87 | return "python" 88 | elif file.endswith(".html"): 89 | return "html" 90 | 91 | return "python" 92 | 93 | 94 | class StackTrace: 95 | """Model a stack trace.""" 96 | 97 | trace: List["StackFrame"] 98 | 99 | def __init__(self, traceback, exception, offset=5, shorten=False, scrubber=None) -> None: 100 | self.traceback = traceback 101 | self.exception = exception 102 | self.trace = [] 103 | self.loop_index = 0 104 | # options 105 | self.offset = offset 106 | self.shorten = shorten 107 | self.scrubber = scrubber 108 | 109 | def generate(self) -> List["StackFrame"]: 110 | """Generate all required data from a given traceback.""" 111 | traceback = [] 112 | for index, tb in enumerate(self.traceback.stack): 113 | traceback.append( 114 | StackFrame( 115 | index, 116 | tb, 117 | variables=tb.locals, 118 | offset=self.offset, 119 | shorten=self.shorten, 120 | ) 121 | ) 122 | 123 | if hasattr(self.exception, "from_obj"): 124 | frame_summary = [ 125 | inspect.getsourcefile(self.exception.from_obj), 126 | inspect.getsourcelines(self.exception.from_obj)[1], 127 | "", 128 | "null", 129 | ] 130 | traceback.append( 131 | StackFrame( 132 | len(traceback), 133 | frame_summary, 134 | variables={}, 135 | offset=self.offset, 136 | shorten=self.shorten, 137 | ) 138 | ) 139 | 140 | self.trace = traceback 141 | return self.trace 142 | 143 | def __iter__(self): 144 | self.loop_index = 0 145 | return self 146 | 147 | def __next__(self): 148 | if self.loop_index < len(self.trace): 149 | result = self.trace[self.loop_index] 150 | self.loop_index += 1 151 | return result 152 | else: 153 | raise StopIteration 154 | 155 | def __len__(self): 156 | return len(self.trace) 157 | 158 | def __getitem__(self, index: int) -> "StackFrame": 159 | return self.trace[index] 160 | 161 | def reverse(self) -> "StackTrace": 162 | """Set stack frame in reversed order. Exception is located at the beginning.""" 163 | self.trace.sort(key=lambda frame: frame.index, reverse=True) 164 | return self 165 | 166 | def unreverse(self) -> "StackTrace": 167 | """Set stack frame in normal order. Exception is located at the end.""" 168 | self.trace.sort(key=lambda frame: frame.index, reverse=False) 169 | return self 170 | 171 | def first(self) -> "StackFrame": 172 | """Get first frame of the stack trace.""" 173 | return self.trace[0] 174 | 175 | def serialize(self) -> dict: 176 | """Serialize all data from the stack trace.""" 177 | stack_data = [] 178 | for frame in self.trace: 179 | frame_data = { 180 | "index": frame.index, 181 | "no": frame.lineno, 182 | "file": frame.file, 183 | "relative_file": frame.relative_file, 184 | "is_vendor": frame.is_vendor, 185 | "language": frame.language, 186 | "start": frame.start_line, 187 | "content": frame.file_contents, 188 | "variables": self.scrubber(frame.variables), 189 | "method": frame.method, 190 | } 191 | stack_data.append(frame_data) 192 | 193 | return stack_data 194 | 195 | def serialize_light(self) -> dict: 196 | """Serialize some data from the stack trace, to get a compact representation.""" 197 | stack_data = [] 198 | for frame in self.trace: 199 | frame_data = { 200 | "index": frame.index, 201 | "file": frame.file, 202 | "line": frame.lineno, 203 | "statement": frame.parent_statement, 204 | } 205 | stack_data.append(frame_data) 206 | return stack_data 207 | -------------------------------------------------------------------------------- /src/exceptionite/Tab.py: -------------------------------------------------------------------------------- 1 | from dotty_dict import dotty 2 | from typing import TYPE_CHECKING, Type, List 3 | 4 | if TYPE_CHECKING: 5 | from .Block import Block 6 | 7 | 8 | class Tab: 9 | 10 | name = "Tab Name" 11 | icon = "" 12 | id = "" 13 | component = "BasicTab" 14 | advertise_content = False 15 | disable_scrubbing = False 16 | 17 | def __init__(self, handler) -> None: 18 | self.handler = handler 19 | self.blocks = {} 20 | tab_options = self.handler.options.get(f"handlers.{self.id}", {}) 21 | self.options = dotty({} if isinstance(tab_options, bool) else tab_options) 22 | assert self.id, "Tab should declare an 'id' attribute !" 23 | 24 | def add_blocks(self, *block_classes: Type["Block"]): 25 | """Register the given block(s) in the tab.""" 26 | for block_class in block_classes: 27 | block_options = self.options.get(block_class.id, {}) 28 | block_options = dotty({} if isinstance(block_options, bool) else block_options) 29 | block = block_class(self, self.handler, block_options) 30 | self.blocks.update({block.id: block}) 31 | return self 32 | 33 | def block(self, id: str) -> "Block": 34 | """Get tab block with the given name.""" 35 | return self.blocks.get(id) 36 | 37 | def active_blocks(self) -> List["Block"]: 38 | """Get active blocks list enabled in options.""" 39 | active_blocks = [] 40 | display_tab = self.handler.options.get(f"handlers.{self.id}", True) 41 | if display_tab is False: 42 | return active_blocks 43 | elif display_tab is True: 44 | return self.blocks.values() 45 | 46 | for block in self.blocks.values(): 47 | display_block = display_tab.get(block.id, True) 48 | if display_block is False: 49 | continue 50 | active_blocks.append(block) 51 | return active_blocks 52 | 53 | def serialize(self) -> dict: 54 | """Serialize data from the tab.""" 55 | raw_data = self.build() 56 | self.data = self.handler.scrub_data(raw_data, self.disable_scrubbing) 57 | return { 58 | "id": self.id, 59 | "name": self.name, 60 | "icon": self.icon, 61 | "component": self.component, 62 | "blocks": list(map(lambda b: b.serialize(), self.active_blocks())), 63 | "data": self.data, 64 | "advertise_content": self.advertise_content, 65 | "has_content": self.has_content(), 66 | } 67 | 68 | def build(self): 69 | return {} 70 | 71 | def has_content(self) -> bool: 72 | """Compute if tab contains content. By default this will check if any of the block have 73 | content.""" 74 | return any( 75 | list(map(lambda b: b.has_content(), self.active_blocks())), 76 | ) 77 | -------------------------------------------------------------------------------- /src/exceptionite/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa F401 2 | from .Handler import Handler, DefaultOptions 3 | from .Block import Block 4 | from .Tab import Tab 5 | from .Action import Action 6 | from .version import __version__ 7 | -------------------------------------------------------------------------------- /src/exceptionite/assets/App.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 198 | -------------------------------------------------------------------------------- /src/exceptionite/assets/Exceptionite.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "vue" 2 | import hljs from 'highlight.js' 3 | import axios from "axios" 4 | import App from "./App.vue" 5 | 6 | // UI 7 | import Badge from "./components/Badge.vue" 8 | import AppButton from "@/components/AppButton.vue" 9 | import CopyButton from "./components/CopyButton.vue" 10 | import tippy from "tippy.js" 11 | import 'tippy.js/dist/tippy.css'; 12 | 13 | // Blocks 14 | import { 15 | BasicBlock, 16 | PackagesUpdatesBlock, 17 | PossibleSolutionsBlock, 18 | StackOverflowBlock 19 | } from "@/components/blocks" 20 | 21 | // Tabs 22 | import { BasicTab, DumpsTab } from "@/components/tabs" 23 | 24 | // Icons 25 | import * as outlineIcons from "@heroicons/vue/outline" 26 | import * as solidIcons from "@heroicons/vue/solid" 27 | 28 | 29 | export default class Exceptionite { 30 | constructor(data) { 31 | this.app = null; 32 | this.data = data; 33 | this.tabCallbacks = []; 34 | } 35 | registerIcons() { 36 | Object.entries(outlineIcons).forEach(entry => { 37 | this.app.component(entry[0], outlineIcons[entry[0]]) 38 | }) 39 | Object.entries(solidIcons).forEach(entry => { 40 | this.app.component(`Solid${entry[0]}`, solidIcons[entry[0]]) 41 | }) 42 | } 43 | registerSharedComponents() { 44 | this.app.component('AppButton', AppButton) 45 | this.app.component('Badge', Badge) 46 | this.app.component('CopyButton', CopyButton) 47 | this.app.component('BasicBlock', BasicBlock) 48 | this.app.component('StackOverflowBlock', StackOverflowBlock) 49 | this.app.component('PossibleSolutionsBlock', PossibleSolutionsBlock) 50 | this.app.component('PackagesUpdatesBlock', PackagesUpdatesBlock) 51 | } 52 | registerBuiltinTabs() { 53 | this.app.component('BasicTab', BasicTab) 54 | this.app.component('DumpsTab', DumpsTab) 55 | } 56 | registerCustomTabs() { 57 | this.tabCallbacks.forEach(callback => callback(this.app, this.data)); 58 | this.tabCallbacks = []; 59 | } 60 | registerTab(callback) { 61 | this.tabCallbacks.push(callback); 62 | } 63 | 64 | start() { 65 | const app = createApp({ 66 | data: () => { 67 | return { 68 | ...this.data, 69 | theme: null, 70 | } 71 | }, 72 | methods: { 73 | copy (text) { 74 | console.log(text) 75 | }, 76 | goToElement (id) { 77 | const offset = 56 + 40 + + 20 // navbar height + exception subnavbar height+ padding 78 | window.scrollTo(0, 0) 79 | var rect = document.getElementById(id).getBoundingClientRect() 80 | window.scrollTo(rect.left , rect.top - offset) 81 | }, 82 | highlightCode (language, code) { 83 | const result = hljs.highlight(code, {language: language}) 84 | return result.value || ' ' 85 | }, 86 | toggleTheme () { 87 | if (this.theme === "dark") { 88 | this.setTheme("light") 89 | } else { 90 | this.setTheme("dark") 91 | } 92 | }, 93 | setTheme(theme) { 94 | if (theme === "dark") { 95 | document.documentElement.classList.add('dark') 96 | } else { 97 | document.documentElement.classList.remove('dark') 98 | } 99 | this.theme = theme 100 | localStorage["exceptionite.theme"] = this.theme 101 | } 102 | }, 103 | mounted () { 104 | if ( 105 | localStorage["exceptionite.theme"] === 'dark' || (!('exceptionite.theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) 106 | ) { 107 | this.setTheme("dark") 108 | } else { 109 | this.setTheme("light") 110 | } 111 | }, 112 | render: () => { 113 | return h(App, this.data) 114 | }, 115 | }) 116 | 117 | app.config.globalProperties.axios = axios 118 | 119 | app.directive('tooltip', { 120 | mounted(el, binding) { 121 | tippy(el, { 122 | content: binding.value 123 | }) 124 | } 125 | }) 126 | 127 | window.app = app 128 | this.app = app 129 | 130 | this.registerIcons() 131 | this.registerSharedComponents() 132 | this.registerBuiltinTabs() 133 | this.registerCustomTabs() 134 | 135 | app.mount('#app') 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/exceptionite/assets/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | :root { 5 | --stack-height: var(--tab-main-height); 6 | } 7 | 8 | .stack { 9 | @apply grid; 10 | grid-template: calc(0.4 * var(--stack-height)) calc(0.6 * var(--stack-height)) / 1fr; 11 | } 12 | 13 | @screen sm { 14 | .stack { 15 | @apply items-stretch; 16 | grid-template: var(--stack-height) / 22rem 1fr; 17 | } 18 | } 19 | 20 | .stack-nav { 21 | @apply h-full; 22 | @apply border-b; 23 | @apply border-gray-300 dark:border-gray-600; 24 | @apply text-xs; 25 | @apply overflow-hidden; 26 | @apply grid; 27 | grid-template: 1fr / 100%; 28 | } 29 | 30 | @screen sm { 31 | .stack-nav { 32 | @apply grid; 33 | grid-template: auto 1fr / 100%; 34 | @apply border-b-0; 35 | @apply border-r; 36 | } 37 | } 38 | 39 | .stack-nav-actions { 40 | @apply hidden; 41 | } 42 | 43 | .stack-nav-arrows { 44 | @apply grid; 45 | @apply justify-center; 46 | @apply items-center; 47 | @apply gap-1; 48 | @apply w-10; 49 | @apply px-3; 50 | } 51 | 52 | .stack-nav-arrow { 53 | @apply text-gray-500; 54 | @apply text-xs; 55 | } 56 | 57 | .stack-nav-arrow:hover { 58 | @apply text-gray-700; 59 | } 60 | 61 | .stack-frames { 62 | @apply overflow-hidden; 63 | @apply border-t; 64 | @apply border-gray-200; 65 | } 66 | 67 | .stack-frames-scroll { 68 | @apply absolute; 69 | @apply inset-0; 70 | @apply overflow-x-hidden; 71 | @apply overflow-y-auto; 72 | } 73 | 74 | .stack-frame-group { 75 | @apply border-b; 76 | @apply border-gray-300; 77 | } 78 | 79 | .stack-frame { 80 | @apply grid; 81 | @apply items-end; 82 | grid-template-columns: 2rem auto auto; 83 | } 84 | 85 | @screen sm { 86 | .stack-frame { 87 | grid-template-columns: 3rem 1fr auto; 88 | } 89 | } 90 | 91 | .stack-frame:not(:first-child) { 92 | @apply -mt-2; 93 | } 94 | 95 | .stack-frame-selected, 96 | .stack-frame-selected .stack-frame-header { 97 | @apply bg-purple-100; 98 | @apply z-10; 99 | } 100 | 101 | .stack-frame-group-vendor .stack-frame-selected, 102 | .stack-frame-group-vendor .stack-frame-selected .stack-frame-header { 103 | /* @apply bg-gray-100; */ 104 | } 105 | 106 | .stack-frame-number { 107 | @apply px-2; 108 | @apply py-4; 109 | @apply text-purple-500; 110 | @apply text-center; 111 | } 112 | 113 | .stack-frame-group-vendor .stack-frame-number { 114 | } 115 | 116 | .stack-frame-header { 117 | @apply -mr-10; 118 | @apply w-full; 119 | } 120 | 121 | .stack-frame-text { 122 | @apply grid; 123 | @apply items-center; 124 | @apply gap-2; 125 | @apply border-l-2; 126 | @apply pl-3; 127 | @apply py-4; 128 | @apply border-purple-300; 129 | @apply text-gray-700; 130 | } 131 | 132 | .stack-frame-group-vendor .stack-frame-text { 133 | @apply border-gray-300; 134 | } 135 | 136 | .stack-frame-selected .stack-frame-text { 137 | @apply border-purple-500; 138 | } 139 | 140 | .stack-frame-group-vendor .stack-frame-selected .stack-frame-text { 141 | @apply border-gray-500; 142 | } 143 | 144 | .stack-frame-line { 145 | @apply pl-2; 146 | @apply pr-1; 147 | @apply py-4; 148 | @apply text-right; 149 | @apply leading-tight; 150 | } 151 | 152 | .stack-main { 153 | @apply grid; 154 | @apply h-full; 155 | @apply overflow-hidden; 156 | @apply bg-gray-100; 157 | grid-template: auto 1fr / 100%; 158 | } 159 | 160 | .stack-main-header { 161 | @apply px-6; 162 | @apply py-2; 163 | @apply border-b; 164 | @apply border-gray-200; 165 | @apply text-xs; 166 | } 167 | 168 | @screen sm { 169 | .stack-main-header { 170 | @apply py-4; 171 | @apply text-base; 172 | } 173 | } 174 | 175 | .stack-main-content { 176 | @apply overflow-hidden; 177 | } 178 | 179 | .stack-viewer { 180 | /*@apply absolute;*/ 181 | @apply inset-0; 182 | @apply flex; 183 | @apply overflow-auto; 184 | @apply text-xs; 185 | } 186 | 187 | .stack-ruler { 188 | position: -webkit-sticky; 189 | position: sticky; 190 | @apply dark:text-gray-300; 191 | @apply flex-none; 192 | @apply left-0; 193 | @apply z-20; 194 | @apply self-stretch; 195 | } 196 | 197 | .stack-lines { 198 | @apply min-h-full; 199 | @apply border-r; 200 | @apply border-gray-200 dark:border-gray-700; 201 | @apply bg-gray-100 dark:bg-gray-900; 202 | @apply py-8; 203 | @apply select-none; 204 | } 205 | 206 | .stack-line { 207 | @apply px-2; 208 | @apply font-mono; 209 | @apply leading-loose; 210 | @apply select-none; 211 | @apply text-black dark:text-gray-400; 212 | } 213 | 214 | .stack-line-highlight { 215 | @apply bg-blue-300 text-blue-600 dark:bg-black dark:text-red-600; 216 | } 217 | 218 | .stack-code { 219 | @apply flex-grow; 220 | @apply py-4; 221 | } 222 | 223 | .stack-code-line { 224 | @apply pl-3; 225 | @apply leading-loose; 226 | } 227 | 228 | .stack-code-line:hover { 229 | @apply bg-blue-100 dark:bg-black; 230 | } 231 | 232 | .stack-code-line .editor-link { 233 | @apply inline-block; 234 | @apply px-2; 235 | @apply opacity-0; 236 | @apply text-purple-400; 237 | } 238 | 239 | .stack-code-line-highlight { 240 | @apply bg-blue-100 dark:bg-black; 241 | } 242 | 243 | .btn { 244 | @apply inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-400 dark:hover:border-gray-200 text-xs leading-4 font-medium rounded-md dark:bg-gray-900 text-gray-600 dark:text-gray-400 dark:hover:text-gray-200 hover:border-blue-500 hover:text-blue-500 focus:outline-none; 245 | } 246 | .btn-disabled { 247 | @apply cursor-not-allowed text-gray-300 dark:text-gray-700 dark:border-gray-700 dark:hover:text-gray-700 dark:hover:border-gray-700 hover:bg-transparent dark:hover:bg-gray-900; 248 | } 249 | 250 | .btn-solution { 251 | @apply bg-transparent dark:bg-transparent border-green-700 text-green-700 dark:text-white dark:border-white dark:hover:bg-blue-900 hover:bg-green-300 hover:border-green-700 hover:text-green-700; 252 | } 253 | 254 | .btn-no-solution { 255 | @apply bg-transparent dark:bg-transparent border-yellow-700 text-yellow-700 dark:text-yellow-900 dark:border-yellow-900 dark:hover:bg-yellow-700 hover:bg-yellow-100 hover:border-yellow-700 hover:text-yellow-700; 256 | } 257 | 258 | /* lists */ 259 | .definition-list { 260 | @apply grid; 261 | @apply gap-x-6; 262 | @apply gap-y-2; 263 | } 264 | 265 | .definition-list .definition-list { 266 | @apply border-l-2; 267 | @apply border-gray-300; 268 | @apply pl-4; 269 | } 270 | 271 | @screen sm { 272 | .definition-list { 273 | grid-template-columns: 8rem 1fr; 274 | } 275 | 276 | .definition-list .definition-list { 277 | grid-template-columns: auto 1fr; 278 | } 279 | } 280 | 281 | @screen lg { 282 | .definition-list { 283 | grid-template-columns: 14rem 1fr; 284 | } 285 | } 286 | 287 | .definition-list-title { 288 | @apply font-semibold; 289 | @apply mb-3; 290 | } 291 | 292 | @screen sm { 293 | .definition-list-title { 294 | margin-left: 9.5rem; 295 | } 296 | } 297 | 298 | @screen lg { 299 | .definition-list-title { 300 | margin-left: 15.5rem; 301 | } 302 | } 303 | 304 | .definition-label { 305 | @apply text-blue-600; 306 | @apply break-words; 307 | @apply leading-tight; 308 | } 309 | 310 | @screen sm { 311 | .definition-label { 312 | @apply text-right; 313 | } 314 | } 315 | 316 | .definition-value { 317 | @apply break-all; 318 | @apply mb-4; 319 | @apply leading-tight; 320 | } 321 | 322 | @screen sm { 323 | .definition-value { 324 | @apply mb-0; 325 | } 326 | } 327 | 328 | .definition-label:empty:after, 329 | .definition-value:empty:after { 330 | content: "—"; 331 | @apply text-gray-300; 332 | } 333 | 334 | .definition-list-empty { 335 | @apply text-gray-300; 336 | } 337 | 338 | @screen sm { 339 | .definition-list-empty { 340 | @apply pl-2; 341 | } 342 | 343 | .definition-list .definition-list .definition-list-empty { 344 | @apply pl-1; 345 | } 346 | } 347 | 348 | /* Navbar */ 349 | .navbar-link { 350 | @apply cursor-pointer text-gray-900 dark:text-gray-300 hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white rounded-md py-2 px-3 inline-flex items-center text-sm font-medium; 351 | } 352 | .navbar-link-disabled { 353 | @apply cursor-not-allowed text-gray-900 dark:text-gray-300 dark:hover:bg-transparent; 354 | } 355 | .navbar-link-icon { 356 | @apply group-hover:text-blue-500 text-gray-400 h-5 w-5; 357 | } 358 | .navbar-link-disabled .navbar-link-icon { 359 | @apply text-gray-200 dark:text-gray-600; 360 | } 361 | .navbar-link-icon-only { 362 | @apply group-hover:text-blue-500 text-black dark:text-gray-400 h-5 w-5; 363 | } 364 | .navbar-link-disabled .navbar-link-icon-only { 365 | @apply text-gray-500 dark:text-gray-600; 366 | } 367 | 368 | .badge { 369 | @apply inline-flex items-center px-2 py-1 rounded text-xs font-medium; 370 | } 371 | .badge-gray { 372 | @apply bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-400; 373 | } 374 | .badge-indigo { 375 | @apply bg-indigo-200 text-indigo-800 dark:bg-indigo-800 dark:text-indigo-200; 376 | } 377 | .badge-blue { 378 | @apply bg-indigo-200 text-blue-800 dark:bg-blue-800 dark:text-blue-200; 379 | } 380 | 381 | @import "./themes/atom-one-dark.css"; 382 | @import "./themes/atom-one-light.css"; 383 | 384 | @tailwind utilities; 385 | -------------------------------------------------------------------------------- /src/exceptionite/assets/app.js: -------------------------------------------------------------------------------- 1 | import Exceptionite from './Exceptionite' 2 | 3 | window.exceptionite = data => { 4 | return new Exceptionite(data) 5 | } -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Alert.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/AppButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Badge.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/BaseActionDialog.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 86 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/CopyButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Exception.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/FirstSolution.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Frame.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 59 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/FrameVars.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/KeyValItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/KeyValList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 135 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/NoSolution.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Pulse.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/ShareDialog.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 163 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/ShareSelector.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 94 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Sponsor.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Stack.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 145 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/Switch.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/blocks/Basic.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 60 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/blocks/PackagesUpdates.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 55 | 56 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/blocks/PossibleSolutions.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 58 | 59 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/blocks/StackOverflow.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/blocks/index.js: -------------------------------------------------------------------------------- 1 | import BasicBlock from "./Basic.vue" 2 | import PackagesUpdatesBlock from "./PackagesUpdates.vue" 3 | import PossibleSolutionsBlock from "./PossibleSolutions.vue" 4 | import StackOverflowBlock from "./StackOverflow.vue" 5 | 6 | export { 7 | BasicBlock, 8 | PackagesUpdatesBlock, 9 | PossibleSolutionsBlock, 10 | StackOverflowBlock, 11 | } -------------------------------------------------------------------------------- /src/exceptionite/assets/components/tabs/Basic.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/tabs/Dumps.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 120 | -------------------------------------------------------------------------------- /src/exceptionite/assets/components/tabs/index.js: -------------------------------------------------------------------------------- 1 | import BasicTab from "./Basic.vue" 2 | import DumpsTab from "./Dumps.vue" 3 | 4 | export { 5 | BasicTab, 6 | DumpsTab, 7 | } -------------------------------------------------------------------------------- /src/exceptionite/assets/editorUrl.js: -------------------------------------------------------------------------------- 1 | export default function editorUrl(editorName, file, lineNumber) { 2 | const editor = editorName 3 | const editors = { 4 | sublime: 'subl://open?url=file://%path&line=%line', 5 | textmate: 'txmt://open?url=file://%path&line=%line', 6 | emacs: 'emacs://open?url=file://%path&line=%line', 7 | macvim: 'mvim://open/?url=file://%path&line=%line', 8 | pycharm: 'pycharm://open?file=%path&line=%line', 9 | idea: 'idea://open?file=%path&line=%line', 10 | vscode: 'vscode://file/%path:%line', 11 | 'vscode-insiders': 'vscode-insiders://file/%path:%line', 12 | 'vscode-remote': 'vscode://vscode-remote/%path:%line', 13 | 'vscode-insiders-remote': 'vscode-insiders://vscode-remote/%path:%line', 14 | vscodium: 'vscodium://file/%path:%line', 15 | atom: 'atom://core/open/file?filename=%path&line=%line', 16 | }; 17 | 18 | if (!Object.keys(editors).includes(editor)) { 19 | console.error( 20 | `'${editor}' is not supported. Support editors are: ${Object.keys(editors).join(', ')}`, 21 | ); 22 | 23 | return null; 24 | } 25 | 26 | return editors[editor] 27 | .replace('%path', encodeURIComponent(file)) 28 | .replace('%line', encodeURIComponent(lineNumber)); 29 | } -------------------------------------------------------------------------------- /src/exceptionite/assets/themes/atom-one-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Atom One Dark by Daniel Gamage 4 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax 5 | 6 | base: #282c34 7 | mono-1: #abb2bf 8 | mono-2: #818896 9 | mono-3: #5c6370 10 | hue-1: #56b6c2 11 | hue-2: #61aeee 12 | hue-3: #c678dd 13 | hue-4: #98c379 14 | hue-5: #e06c75 15 | hue-5-2: #be5046 16 | hue-6: #d19a66 17 | hue-6-2: #e6c07b 18 | 19 | */ 20 | 21 | .dark .hljs { 22 | color: #abb2bf; 23 | /* background: #282c34; to match general page theme bg-gray-800 instead*/ 24 | } 25 | 26 | .dark .hljs-comment, 27 | .dark .hljs-quote { 28 | color: #5c6370; 29 | font-style: italic; 30 | } 31 | 32 | .dark .hljs-doctag, 33 | .dark .hljs-keyword, 34 | .dark .hljs-formula { 35 | color: #c678dd; 36 | } 37 | 38 | .dark .hljs-section, 39 | .dark .hljs-name, 40 | .dark .hljs-selector-tag, 41 | .dark .hljs-deletion, 42 | .dark .hljs-subst { 43 | color: #e06c75; 44 | } 45 | 46 | .dark .hljs-literal { 47 | color: #56b6c2; 48 | } 49 | 50 | .dark .hljs-string, 51 | .dark .hljs-regexp, 52 | .dark .hljs-addition, 53 | .dark .hljs-attribute, 54 | .dark .hljs-meta .hljs-string { 55 | color: #98c379; 56 | } 57 | 58 | .dark .hljs-attr, 59 | .dark .hljs-variable, 60 | .dark .hljs-template-variable, 61 | .dark .hljs-type, 62 | .dark .hljs-selector-class, 63 | .dark .hljs-selector-attr, 64 | .dark .hljs-selector-pseudo, 65 | .dark .hljs-number { 66 | color: #d19a66; 67 | } 68 | 69 | .dark .hljs-symbol, 70 | .dark .hljs-bullet, 71 | .dark .hljs-link, 72 | .dark .hljs-meta, 73 | .dark .hljs-selector-id, 74 | .dark .hljs-title { 75 | color: #61aeee; 76 | } 77 | 78 | .dark .hljs-built_in, 79 | .dark .hljs-title.class_, 80 | .dark .hljs-class .hljs-title { 81 | color: #e6c07b; 82 | } 83 | 84 | .dark .hljs-emphasis { 85 | font-style: italic; 86 | } 87 | 88 | .dark .hljs-strong { 89 | font-weight: bold; 90 | } 91 | 92 | .dark .hljs-link { 93 | text-decoration: underline; 94 | } 95 | -------------------------------------------------------------------------------- /src/exceptionite/assets/themes/atom-one-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Atom One Light by Daniel Gamage 4 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 5 | 6 | base: #fafafa 7 | mono-1: #383a42 8 | mono-2: #686b77 9 | mono-3: #a0a1a7 10 | hue-1: #0184bb 11 | hue-2: #4078f2 12 | hue-3: #a626a4 13 | hue-4: #50a14f 14 | hue-5: #e45649 15 | hue-5-2: #c91243 16 | hue-6: #986801 17 | hue-6-2: #c18401 18 | 19 | */ 20 | 21 | .hljs { 22 | color: #383a42; 23 | background: #fafafa; 24 | } 25 | 26 | .hljs-comment, 27 | .hljs-quote { 28 | color: #a0a1a7; 29 | font-style: italic; 30 | } 31 | 32 | .hljs-doctag, 33 | .hljs-keyword, 34 | .hljs-formula { 35 | color: #a626a4; 36 | } 37 | 38 | .hljs-section, 39 | .hljs-name, 40 | .hljs-selector-tag, 41 | .hljs-deletion, 42 | .hljs-subst { 43 | color: #e45649; 44 | } 45 | 46 | .hljs-literal { 47 | color: #0184bb; 48 | } 49 | 50 | .hljs-string, 51 | .hljs-regexp, 52 | .hljs-addition, 53 | .hljs-attribute, 54 | .hljs-meta .hljs-string { 55 | color: #50a14f; 56 | } 57 | 58 | .hljs-attr, 59 | .hljs-variable, 60 | .hljs-template-variable, 61 | .hljs-type, 62 | .hljs-selector-class, 63 | .hljs-selector-attr, 64 | .hljs-selector-pseudo, 65 | .hljs-number { 66 | color: #986801; 67 | } 68 | 69 | .hljs-symbol, 70 | .hljs-bullet, 71 | .hljs-link, 72 | .hljs-meta, 73 | .hljs-selector-id, 74 | .hljs-title { 75 | color: #4078f2; 76 | } 77 | 78 | .hljs-built_in, 79 | .hljs-title.class_, 80 | .hljs-class .hljs-title { 81 | color: #c18401; 82 | } 83 | 84 | .hljs-emphasis { 85 | font-style: italic; 86 | } 87 | 88 | .hljs-strong { 89 | font-weight: bold; 90 | } 91 | 92 | .hljs-link { 93 | text-decoration: underline; 94 | } 95 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/Environment.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import platform 3 | import socket 4 | import os 5 | 6 | from ..Block import Block 7 | 8 | 9 | class Environment(Block): 10 | id = "environment" 11 | name = "System Environment" 12 | icon = "TerminalIcon" 13 | 14 | def build(self): 15 | python_version = ( 16 | f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" 17 | ) 18 | default_encoding = sys.getdefaultencoding() 19 | file_system_encoding = sys.getfilesystemencoding() 20 | os_name = platform.system() 21 | if os_name == "Darwin": 22 | os_name = "macOS" 23 | 24 | # when VPN is enabled it can fails for some VPN clients on macOS 25 | try: 26 | ip = socket.gethostbyname(socket.gethostname()) 27 | except socket.gaierror: 28 | print( 29 | "Exceptionite did not manage to fetch the IP address. Disable you VPN or add " 30 | + "'127.0.0.1 YOUR_HOSTNAME' line in /etc/hosts file." 31 | ) 32 | ip = "Error fetching the IP address (open your terminal)" 33 | 34 | return { 35 | "Python Version": python_version, 36 | "Python Interpreter": sys.executable, 37 | "Virtual env": os.getenv("VIRTUAL_ENV"), 38 | "Python argv": sys.argv, 39 | "Working Dir": os.getcwd(), 40 | "OS": os_name, 41 | "Arch": platform.architecture()[0], 42 | "Host Name": socket.gethostname(), 43 | "IP": ip, 44 | "File System Encoding": file_system_encoding, 45 | "Default Encoding": default_encoding, 46 | } 47 | 48 | def has_content(self): 49 | return True 50 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/Git.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import subprocess 3 | 4 | from ..Block import Block 5 | 6 | 7 | class Git(Block): 8 | id = "git" 9 | name = "Git" 10 | icon = "ShareIcon" 11 | disable_scrubbing = True 12 | 13 | def build(self): 14 | git_version = subprocess.check_output(shlex.split("git --version")).strip() 15 | try: 16 | commit = subprocess.check_output( 17 | shlex.split("git rev-parse HEAD"), stderr=subprocess.STDOUT 18 | ).strip() 19 | except subprocess.CalledProcessError: 20 | # not a git repository 21 | return { 22 | "commit": "", 23 | "branch": "", 24 | "git_version": git_version.decode("utf-8"), 25 | "remote": "", 26 | } 27 | branch = subprocess.check_output( 28 | shlex.split("git rev-parse --abbrev-ref HEAD"), stderr=subprocess.STDOUT 29 | ).strip() 30 | try: 31 | remote = subprocess.check_output( 32 | shlex.split("git config --get remote.origin.url"), stderr=subprocess.STDOUT 33 | ).strip() 34 | except subprocess.CalledProcessError: 35 | remote = b"" 36 | 37 | return { 38 | "commit": commit.decode("utf-8"), 39 | "branch": branch.decode("utf-8"), 40 | "git_version": git_version.decode("utf-8"), 41 | "remote": remote.decode("utf-8"), 42 | } 43 | 44 | def has_content(self): 45 | return True 46 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/Packages.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | from ..Block import Block 4 | 5 | 6 | class Packages(Block): 7 | id = "packages" 8 | name = "Installed Packages" 9 | icon = "PuzzleIcon" 10 | disable_scrubbing = True 11 | 12 | def build(self): 13 | packages = {} 14 | for package in pkg_resources.working_set: 15 | packages.update({package.key: package.version}) 16 | return packages 17 | 18 | def has_content(self): 19 | return True 20 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/PackagesUpdates.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | import requests 3 | 4 | from ..Block import Block 5 | 6 | 7 | def get_latest_version(name): 8 | r = requests.get(f"https://pypi.org/pypi/{name}/json") 9 | if r.status_code == 200: 10 | version = r.json()["info"]["version"] 11 | return version 12 | return None 13 | 14 | 15 | class PackagesUpdates(Block): 16 | id = "packages_updates" 17 | name = "Packages to update" 18 | icon = "ArrowCircleUpIcon" 19 | component = "PackagesUpdatesBlock" 20 | empty_msg = "Selected packages are up to date !" 21 | disable_scrubbing = True 22 | 23 | def build(self): 24 | installed_packages = { 25 | package.key: package.version for package in pkg_resources.working_set 26 | } 27 | 28 | packages_to_check = self.options.get("list", ["exceptionite"]) 29 | packages = {} 30 | if packages_to_check: 31 | for package_name in packages_to_check: 32 | current_version = installed_packages.get(package_name) 33 | latest_version = get_latest_version(package_name) 34 | if current_version != latest_version: 35 | packages.update( 36 | { 37 | package_name: { 38 | "current": installed_packages.get(package_name), 39 | "latest": latest_version, 40 | } 41 | } 42 | ) 43 | 44 | return packages 45 | 46 | def has_content(self): 47 | return len(self.data.keys()) > 0 48 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/PossibleSolutions.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import urllib.parse 4 | 5 | from ..Block import Block 6 | from .. import solutions 7 | 8 | 9 | class PossibleSolutions(Block): 10 | 11 | id = "possible_solutions" 12 | name = "Possible Solutions" 13 | component = "PossibleSolutionsBlock" 14 | advertise_content = True 15 | disable_scrubbing = True 16 | empty_msg = "No solution found for this error." 17 | icon = "LightBulbIcon" 18 | 19 | def __init__(self, tab, handler, options): 20 | super().__init__(tab, handler, options) 21 | self.registered_solutions = [] 22 | self.register( 23 | *solutions.PythonSolutions.get(), 24 | ) 25 | 26 | def build(self): 27 | possible_solutions = [] 28 | for solution in self.registered_solutions: 29 | r = re.compile(solution.regex()) 30 | doc_link = None 31 | if r.search(self.handler.message()): 32 | description = solution.description() 33 | title = solution.title() 34 | matches = [m.groupdict() for m in r.finditer(self.handler.message())] 35 | if hasattr(solution, "documentation_link"): 36 | if solution.documentation_link(): 37 | doc_link = solution.documentation_link() 38 | for code, replacement in matches[0].items(): 39 | description = description.replace(":" + code, replacement) 40 | title = title.replace(":" + code, replacement) 41 | 42 | possible_solutions.append( 43 | {"title": title, "description": description, "doc_link": doc_link} 44 | ) 45 | 46 | # build request_link 47 | request_link = "" 48 | if not possible_solutions: 49 | request_link = self.get_request_link() 50 | 51 | return { 52 | "first": possible_solutions[0] if len(possible_solutions) > 0 else None, 53 | "solutions": possible_solutions, 54 | "request_link": request_link, 55 | } 56 | 57 | def get_request_link(self): 58 | params = { 59 | "title": f"Add exceptionite solution for `{self.handler.exception()}`", 60 | "body": f"A solution is missing:\nException namespace: `{self.handler.namespace()}`\nError message:\n```\n{self.handler.message()}\n```", # noqa: E501 61 | "labels": "solution-request", 62 | } 63 | return f"https://github.com/MasoniteFramework/exceptionite/issues/new/?{urllib.parse.urlencode(params)}" # noqa: E501 64 | 65 | def register(self, *solutions): 66 | self.registered_solutions += solutions 67 | return self 68 | 69 | def has_content(self): 70 | return len(self.data.get("solutions")) > 0 71 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/StackOverflow.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import urllib 3 | from ..Block import Block 4 | 5 | 6 | class StackOverflow(Block): 7 | id = "stackoverflow" 8 | name = "Stack Overflow Answers" 9 | component = "StackOverflowBlock" 10 | tags = ["python"] 11 | api_url = "https://api.stackexchange.com/2.2/" 12 | empty_msg = "No solution found on Stack Overflow." 13 | disable_scrubbing = True 14 | 15 | def get_tags(self): 16 | return ";".join(self.tags) 17 | 18 | def build(self): 19 | query = urllib.parse.urlencode( 20 | { 21 | "order": "asc", 22 | "sort": "relevance", 23 | "q": self.handler.message(), 24 | "body": self.handler.message(), 25 | "accepted": True, 26 | "tagged": self.get_tags(), 27 | "site": "stackoverflow", 28 | } 29 | ) 30 | 31 | try: 32 | response = requests.get(f"{self.api_url}search/advanced?{query}").json() 33 | except requests.exceptions.ConnectionError: 34 | return {} 35 | 36 | accepted_answer_ids = [] 37 | 38 | if not response.get("items"): 39 | query = urllib.parse.urlencode( 40 | { 41 | "order": "desc", 42 | "sort": "relevance", 43 | "intitle": self.handler.exception(), 44 | "site": "stackoverflow", 45 | "filter": "!-*jbN-(0_ynL", 46 | "tagged": self.get_tags(), 47 | "key": "k7C3UwXDt3J0xOpri8RPgA((", 48 | } 49 | ) 50 | response = requests.get(f"{self.api_url}search/advanced?{query}").json() 51 | 52 | for question in response.get("items", []): 53 | if "accepted_answer_id" in question: 54 | accepted_answer_ids.append(str(question["accepted_answer_id"])) 55 | 56 | query = urllib.parse.urlencode( 57 | {"order": "desc", "sort": "activity", "site": "stackoverflow"} 58 | ) 59 | answers = requests.get( 60 | f"{self.api_url}answers/{';'.join(accepted_answer_ids)}?{query}" 61 | ).json() 62 | 63 | return {"questions": response["items"], "answers": answers} 64 | 65 | def has_content(self): 66 | return len(self.data.get("questions", [])) > 0 67 | -------------------------------------------------------------------------------- /src/exceptionite/blocks/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E501 2 | from .Environment import Environment 3 | from .Git import Git 4 | from .Packages import Packages 5 | from .PackagesUpdates import PackagesUpdates 6 | from .PossibleSolutions import PossibleSolutions 7 | from .StackOverflow import StackOverflow 8 | -------------------------------------------------------------------------------- /src/exceptionite/django/ExceptioniteReporter.py: -------------------------------------------------------------------------------- 1 | from .. import Handler, Block 2 | from .options import OPTIONS 3 | from ..solutions import DjangoSolutions 4 | 5 | 6 | class ContextBlock(Block): 7 | id = "django-request" 8 | name = "Context" 9 | icon = "DesktopComputerIcon" 10 | has_sections = True 11 | 12 | def build(self): 13 | from django.utils.version import get_version 14 | 15 | return { 16 | "Django": { 17 | "Version": get_version(), 18 | }, 19 | "Request Info": { 20 | "Path": self.handler.request.path, 21 | "GET": self.handler.request.GET, 22 | "POST": self.handler.request.POST, 23 | "Files": self.handler.request.FILES, 24 | "Cookies": self.handler.request.COOKIES, 25 | "Request Method": self.handler.request.method, 26 | }, 27 | } 28 | 29 | 30 | class ExceptioniteReporter: 31 | def __init__(self, request, exc_type, exc_value, tb): 32 | self.request = request 33 | self.exception = exc_value 34 | 35 | def get_traceback_html(self): 36 | from django.http.response import HttpResponse 37 | 38 | handler = Handler() 39 | handler.start(self.exception) 40 | handler.render("terminal") 41 | handler.request = self.request 42 | handler.renderer("web").tab("context").add_blocks(ContextBlock) 43 | handler.renderer("web").tab("solutions").block("possible_solutions").register( 44 | *DjangoSolutions().get() 45 | ) 46 | handler.set_options(OPTIONS) 47 | return HttpResponse(handler.render("web")) 48 | 49 | def get_traceback_json(self): 50 | from django.http.response import JsonResponse 51 | 52 | handler = Handler() 53 | handler.start(self.exception) 54 | handler.render("terminal") 55 | return JsonResponse(handler.render("json")) 56 | 57 | 58 | class Exceptionite404Reporter: 59 | """Handle Django 404 errors specifically in debug mode.""" 60 | 61 | def __init__(self): 62 | from mock import patch 63 | from django.http import HttpResponseNotFound 64 | 65 | patcher = patch( 66 | "django.views.debug.technical_404_response", 67 | lambda request, exception: HttpResponseNotFound( 68 | ExceptioniteReporter(request, None, exception, None).get_traceback_html() 69 | ), 70 | ) 71 | patcher.start() 72 | -------------------------------------------------------------------------------- /src/exceptionite/django/__init__.py: -------------------------------------------------------------------------------- 1 | from .drf_exception_handler import drf_exception_handler 2 | from .ExceptioniteReporter import ExceptioniteReporter, Exceptionite404Reporter 3 | -------------------------------------------------------------------------------- /src/exceptionite/django/drf_exception_handler.py: -------------------------------------------------------------------------------- 1 | from .ExceptioniteReporter import ExceptioniteReporter 2 | 3 | 4 | def drf_exception_handler(exc, context): 5 | from rest_framework.views import exception_handler 6 | 7 | # Call REST framework's default exception handler first, 8 | # to get the standard error response. 9 | response = exception_handler(exc, context) 10 | 11 | # Now add the HTTP status code to the response. 12 | if response is not None: 13 | response.data["status_code"] = response.status_code 14 | 15 | # Handle exceptions from drf with exceptionite 16 | request = context["request"] 17 | content_type = request.accepted_renderer.media_type 18 | reporter = ExceptioniteReporter(request, None, exc, None) 19 | if content_type == "text/html": 20 | return reporter.get_traceback_html() 21 | elif content_type == "application/json": 22 | return reporter.get_traceback_json() 23 | 24 | return response 25 | -------------------------------------------------------------------------------- /src/exceptionite/django/options.py: -------------------------------------------------------------------------------- 1 | OPTIONS = { 2 | "options": { 3 | "editor": "vscode", 4 | "search_url": "https://www.google.com/search?q=", 5 | "links": { 6 | "doc": "https://docs.djangoproject.com/en/4.0/", 7 | "repo": "https://github.com/django/django", 8 | }, 9 | "stack": {"offset": 8, "shorten": True}, 10 | "hide_sensitive_data": True, 11 | }, 12 | "handlers": { 13 | "context": True, 14 | "solutions": {"stackoverflow": False, "possible_solutions": True}, 15 | "recommendations": {"packages_updates": {"list": ["exceptionite"]}}, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/exceptionite/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConfigurationException(Exception): 2 | pass 3 | 4 | 5 | class ContextParsingException(Exception): 6 | pass 7 | 8 | 9 | class TabNotFound(Exception): 10 | pass 11 | -------------------------------------------------------------------------------- /src/exceptionite/flask/ExceptioniteReporter.py: -------------------------------------------------------------------------------- 1 | from .. import Handler, Block 2 | from .options import OPTIONS 3 | 4 | 5 | class ContextBlock(Block): 6 | id = "flask" 7 | name = "Flask" 8 | icon = "DesktopComputerIcon" 9 | 10 | def build(self): 11 | return { 12 | "Path": self.handler.request.path, 13 | "Input": dict(self.handler.request.args), 14 | "Request Method": self.handler.request.method, 15 | } 16 | 17 | 18 | class ExceptioniteReporter: 19 | def __init__(self, exception, request): 20 | self.exception = exception 21 | self.request = request 22 | handler = Handler() 23 | handler.renderer("web").tab("context").add_blocks(ContextBlock) 24 | handler.request = self.request 25 | handler.set_options(OPTIONS) 26 | 27 | handler.start(self.exception) 28 | self.handler = handler 29 | 30 | def html(self): 31 | return self.handler.render("web") 32 | 33 | def json(self): 34 | return self.handler.render("json") 35 | 36 | def terminal(self): 37 | return self.handler.render("terminal") 38 | -------------------------------------------------------------------------------- /src/exceptionite/flask/__init__.py: -------------------------------------------------------------------------------- 1 | from .ExceptioniteReporter import ExceptioniteReporter 2 | -------------------------------------------------------------------------------- /src/exceptionite/flask/options.py: -------------------------------------------------------------------------------- 1 | OPTIONS = { 2 | "options": { 3 | "editor": "vscode", 4 | "search_url": "https://www.google.com/search?q=", 5 | "links": { 6 | "doc": "https://flask.palletsprojects.com", 7 | "repo": "https://github.com/pallets/flask/", 8 | }, 9 | "stack": {"offset": 8, "shorten": True}, 10 | "hide_sensitive_data": True, 11 | }, 12 | "handlers": { 13 | "context": True, 14 | "solutions": {"stackoverflow": False, "possible_solutions": True}, 15 | "recommendations": {"packages_updates": {"list": ["exceptionite", "flask"]}}, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/exceptionite/options.py: -------------------------------------------------------------------------------- 1 | DEFAULT_OPTIONS = { 2 | "options": { 3 | "editor": "vscode", 4 | "search_url": "https://www.google.com/search?q=", 5 | "links": { 6 | "doc": "https://docs.masoniteproject.com", 7 | "repo": "https://github.com/MasoniteFramework/masonite", 8 | }, 9 | "stack": {"offset": 8, "shorten": True}, 10 | "hide_sensitive_data": True, 11 | }, 12 | "handlers": { 13 | "context": True, 14 | "solutions": {"stackoverflow": False, "possible_solutions": True}, 15 | "recommendations": {"packages_updates": {"list": ["exceptionite"]}}, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/exceptionite/renderers/JSONRenderer.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | from ..Handler import Handler 5 | 6 | 7 | class JSONRenderer: 8 | """Renderer used to render exception as JSON payload.""" 9 | 10 | def __init__(self, handler: "Handler") -> None: 11 | self.handler = handler 12 | self.data: dict = {} 13 | 14 | def build(self): 15 | return { 16 | "exception": { 17 | "type": self.handler.exception(), 18 | "namespace": self.handler.namespace(), 19 | }, 20 | "message": self.handler.message(), 21 | "stacktrace": self.handler.stacktrace().reverse().serialize_light(), 22 | } 23 | 24 | def render(self) -> str: 25 | """Render the JSON payload.""" 26 | self.data = self.build() 27 | return self.data 28 | -------------------------------------------------------------------------------- /src/exceptionite/renderers/TerminalRenderer.py: -------------------------------------------------------------------------------- 1 | from colorama import init as init_colorama, Fore, Style 2 | from typing import TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from ..Handler import Handler 6 | 7 | 8 | COLORS_PREFIX = { 9 | "error": Fore.RED + Style.BRIGHT, 10 | "info": Fore.CYAN + Style.BRIGHT, 11 | "success": Fore.GREEN + Style.BRIGHT, 12 | } 13 | 14 | 15 | def color_output(color: str, output: str) -> str: 16 | return f"{COLORS_PREFIX.get(color)}{output}" + "\033[0m" 17 | 18 | 19 | def get_frame_line_output(frame, index: int = None) -> str: 20 | frame_method = color_output("info", f"{frame.method}()") 21 | frame_line = f"{frame.relative_file or frame.file}: L{frame.lineno} in {frame_method}" 22 | if index: 23 | frame_index_colored = index if frame.is_vendor else color_output("success", index) 24 | return f"{frame_index_colored} {frame_line}" 25 | return frame_line 26 | 27 | 28 | class TerminalRenderer: 29 | """Renderer used to print an exception nicely in the terminal. It will render stack 30 | trace too.""" 31 | 32 | def __init__(self, handler: "Handler") -> None: 33 | self.handler = handler 34 | 35 | def render(self) -> str: 36 | """Print exception and stack trace nicely in terminal.""" 37 | init_colorama() 38 | 39 | # start printing exception to terminal 40 | print("") 41 | print("") 42 | colored_exception_type = color_output("error", self.handler.exception()) 43 | print(f" {colored_exception_type}: {self.handler.message()}") 44 | print("") 45 | 46 | # show most recent frame of the stack 47 | stacktrace = self.handler.stacktrace().reverse() 48 | first_frame = stacktrace.first() 49 | first_frame_code = first_frame.file_contents 50 | print(f" {get_frame_line_output(first_frame)}") 51 | print("") 52 | 53 | # get highest line number to align numbers with padding 54 | max_no_len = len(str(max(first_frame_code.keys()))) 55 | for lineno, code_line in first_frame_code.items(): 56 | lineno_str = str(lineno) 57 | if len(lineno_str) < max_no_len: 58 | for space in range(max_no_len - len(lineno_str)): 59 | lineno_str += " " 60 | code_line = code_line.replace(" ", " ") 61 | # highlight line where exception raised 62 | if lineno == first_frame.offending_line: 63 | print(color_output("error", f" > {lineno_str} | {code_line}")) 64 | else: 65 | print(f" {lineno_str} | {code_line}") 66 | print("") 67 | 68 | # show other frames 69 | title = color_output("info", f"Stack Trace ({len(stacktrace)}):") 70 | print(f" {title}") 71 | print("") 72 | for index, frame in enumerate(stacktrace): 73 | frame_index = str(index + 1) 74 | print(f" # {get_frame_line_output(frame, frame_index)}") 75 | return "" 76 | -------------------------------------------------------------------------------- /src/exceptionite/renderers/WebRenderer.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import os 3 | from jinja2 import Environment, PackageLoader 4 | from typing import Type, TYPE_CHECKING, List, Callable 5 | 6 | 7 | if TYPE_CHECKING: 8 | from ..Tab import Tab 9 | from ..Handler import Handler 10 | from ..Action import Action 11 | 12 | from ..tabs import ContextTab, SolutionsTab, RecommendationsTab 13 | from ..exceptions import ConfigurationException, TabNotFound 14 | from ..Block import Block 15 | 16 | 17 | class WebRenderer: 18 | """Renderer used to render exception as a beautiful extendable HTML error page which ease 19 | debugging.""" 20 | 21 | reserved_tabs = ["context"] 22 | 23 | def __init__(self, handler: "Handler") -> None: 24 | self.handler = handler 25 | # error page interface options 26 | self.tabs: dict = OrderedDict() 27 | self.actions: dict = {} 28 | self.context: dict = {} 29 | # setup base interface 30 | self.add_tabs(ContextTab, SolutionsTab, RecommendationsTab) 31 | 32 | def build(self) -> dict: 33 | """Build the handled exception context to inject into the error page.""" 34 | from .. import __version__ 35 | 36 | enabled_tabs = self.enabled_tabs() 37 | return { 38 | "config": { 39 | **self.handler.options.to_dict(), 40 | "absolute_path": os.getcwd(), 41 | "version": __version__, 42 | }, 43 | "exception": { 44 | "type": self.handler.exception(), 45 | "message": self.handler.message(), 46 | "namespace": self.handler.namespace(), 47 | "stacktrace": self.handler.stacktrace().reverse().serialize(), 48 | }, 49 | "tabs": [self.handler.scrub_data(tab.serialize()) for tab in enabled_tabs], 50 | "actions": [action.serialize() for action in self.actions.values()], 51 | } 52 | 53 | def render(self) -> str: 54 | """Render the HTML error page.""" 55 | self.data = self.build() 56 | path = os.path.join( 57 | os.path.dirname(os.path.dirname(__file__)), "templates", "exceptionite.js" 58 | ) 59 | with open(path, "r", encoding="utf-8") as f: 60 | script = f.read() 61 | 62 | env = Environment(loader=PackageLoader("exceptionite", "templates")) 63 | 64 | template = env.get_template("exception.html") 65 | return template.render({"data": self.data, "script": script}) 66 | 67 | def add_tabs(self, *tab_classes: Type["Tab"]) -> "Handler": 68 | """Register a tab in the HTML error page.""" 69 | for tab_class in tab_classes: 70 | tab = tab_class(self.handler) 71 | if tab.id in self.reserved_tabs and tab.id in self.tabs: 72 | raise ConfigurationException( 73 | f"exceptionite: {tab.id} is a reserved name. This tab can't be overriden." 74 | ) 75 | self.tabs.update({tab.id: tab}) 76 | return self 77 | 78 | def add_actions(self, *action_classes: Type["Action"]) -> "Handler": 79 | """Register an action in the HTML error page.""" 80 | for action_class in action_classes: 81 | action = action_class(self.handler) 82 | self.actions.update({action.id: action}) 83 | return self 84 | 85 | def tab(self, id: str) -> "Tab": 86 | """Get registered tab with the given id.""" 87 | try: 88 | return self.tabs[id] 89 | except KeyError: 90 | raise TabNotFound(f"Tab not found: {id}") 91 | 92 | def enabled_tabs(self) -> List["Tab"]: 93 | """Get enabled tabs from options""" 94 | return [ 95 | tab 96 | for tab in self.tabs.values() 97 | if self.handler.options.get("handlers").get(tab.id, True) 98 | ] 99 | 100 | def run_action(self, action_id: str, options: dict = {}) -> dict: 101 | """Run the given action with options if any""" 102 | action = self.actions.get(action_id) 103 | return action.run(options) 104 | 105 | def add_context(self, name: str, data: "dict|Callable", icon: str = None) -> "WebRenderer": 106 | """Quick shortcut method to add a context block into 'context' tab.""" 107 | 108 | custom_block = Block 109 | custom_block.id = f"context.id.{name.lower()}" 110 | custom_block.name = name 111 | custom_block.icon = icon 112 | if callable(data): 113 | custom_block.build = data 114 | else: 115 | custom_block.build = lambda self: data 116 | self.tab("context").add_blocks(custom_block) 117 | return self 118 | -------------------------------------------------------------------------------- /src/exceptionite/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E501 2 | from .WebRenderer import WebRenderer 3 | from .TerminalRenderer import TerminalRenderer 4 | from .JSONRenderer import JSONRenderer 5 | -------------------------------------------------------------------------------- /src/exceptionite/solutions.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E501 2 | 3 | 4 | class PythonSolutions: 5 | @classmethod 6 | def get(cls): 7 | return [ 8 | DictionaryUpdateSequence(), 9 | DictionaryUpdateSequenceWithList(), 10 | ClassMethodExists(), 11 | UnexpectedEndBlock(), 12 | QueryDefaultValue(), 13 | NoRelationExistsInDatabase(), 14 | NoColumnExistsOnWhere(), 15 | NoColumnExistsOnWhereSQLite(), 16 | NoColumnExistsOnSelect(), 17 | UnsupportedOperand(), 18 | DivisionByZeroError(), 19 | GetAttributeObject(), 20 | ModuleHasNoAttribute(), 21 | ModuleNotCallable(), 22 | NoModuleNamed(), 23 | Syntax(), 24 | ImportIssue(), 25 | Undefined(), 26 | WrongParameterCount(), 27 | WrongConstructorParameterCount(), 28 | ObjectNotCallable(), 29 | SubscriptableIssue(), 30 | StringIndicesMustBeIntegers(), 31 | MySQLConnectionRefused(), 32 | PostgresConnectionRefused(), 33 | PostgresConnectionFailed(), 34 | DatabaseDoesNotExist(), 35 | DatabaseUserDoesNotExist(), 36 | ] 37 | 38 | 39 | class MasoniteSolutions: 40 | @classmethod 41 | def get(cls): 42 | return [ 43 | ClassModelMethodExists(), 44 | ImportIssueWithController(), 45 | IncorrectControllerName(), 46 | IncorrectlyDefinedRoute(), 47 | RouteNameNotFound(), 48 | IncludedTemplateNotFound(), 49 | ContainerKeyNotFoundRegister(), 50 | ContainerKeyNotFoundServiceProvider(), 51 | NotFound404(), 52 | InvalidRouteMethodType(), 53 | ModelNotFound(), 54 | DatabaseDriverNotFound(), 55 | DriverNotFound(), 56 | MethodNotAllowed(), 57 | ] 58 | 59 | 60 | class DjangoSolutions: 61 | @classmethod 62 | def get(cls): 63 | return [ 64 | DjangoTemplateNotFound(), 65 | ContextShouldBeList(), 66 | RenderArgumentsOutOfOrder(), 67 | ] 68 | 69 | 70 | class DictionaryUpdateSequence: 71 | def title(self): 72 | return "Updating a dictionary with a set. " 73 | 74 | def description(self): 75 | return ( 76 | "Looks like you are trying to update a dictionary but are actually using a set. " 77 | "Double check what you are passing into the update method" 78 | ) 79 | 80 | def regex(self): 81 | return r"dictionary update sequence element #0 has length 3; 2 is required" 82 | 83 | 84 | class DictionaryUpdateSequenceWithList: 85 | def title(self): 86 | return "Updating a dictionary with a list or set. " 87 | 88 | def description(self): 89 | return ( 90 | "Looks like you are trying to update a dictionary but are actually using a list or a set. " 91 | "Double check what you are passing into the dictionaries update method" 92 | ) 93 | 94 | def regex(self): 95 | return r"cannot convert dictionary update sequence element #0 to a sequence" 96 | 97 | 98 | class ClassMethodExists: 99 | def title(self): 100 | return "Check the class method exists" 101 | 102 | def description(self): 103 | return ( 104 | "Check the :method attribute exists on the ':class' class. If this is a class you made then check the file its in and see if the method exists." 105 | "If this is a third party class then refer to the documentation." 106 | ) 107 | 108 | def regex(self): 109 | return r"^class \'(?P([\w]*))\' has no attribute (?P(\w+))" 110 | 111 | 112 | class ClassModelMethodExists: 113 | def title(self): 114 | return "Model method does not exist" 115 | 116 | def description(self): 117 | return "Could not find the ':method' method on the model class. Please check spelling. If this is a method you expect to be on the builder class then check the ORM documentation" 118 | 119 | def regex(self): 120 | return r"^class model \'(?P([\w]*))\' has no attribute (?P(\w+))" 121 | 122 | 123 | class ImportIssueWithController: 124 | def title(self): 125 | return "Import Error In Controller" 126 | 127 | def description(self): 128 | return ( 129 | "The :class controller could not be loaded into the route correctly. Check any recent imports or all imports in the controller. " 130 | "Worst case is you can import the controller directly in the route and you should get a python error." 131 | ) 132 | 133 | def regex(self): 134 | return r"named (?P([\w]*)) has been found in" 135 | 136 | 137 | class IncorrectControllerName: 138 | def title(self): 139 | return "Mispelled Controller name" 140 | 141 | def description(self): 142 | return "The :class controller could be mispelled. Check your routes file for :class and make sure that is the correct spelling." 143 | 144 | def regex(self): 145 | return r"named (?P([\w]*)) has been found in" 146 | 147 | 148 | class IncorrectlyDefinedRoute: 149 | def title(self): 150 | return "Check the controller and action is set correctly on the route." 151 | 152 | def description(self): 153 | return "Check the definition on the controller is correct. If using string controllers it should be in the format of 'Controller@action'" 154 | 155 | def regex(self): 156 | return r"named (?P([\w]*)) has been found in" 157 | 158 | def documentation_link(self): 159 | return "https://docs.masoniteproject.com/the-basics/routing#creating-a-route" 160 | 161 | 162 | class RouteNameNotFound: 163 | def title(self): 164 | return "Check the the name exists in your routes file" 165 | 166 | def description(self): 167 | return """Check the routes file and make sure there is a route with the ".name(':name')\" method. You can also run `python craft routes:list` to see a table of routes. Check for your named route in that table.""" 168 | 169 | def regex(self): 170 | return r"Could not find route with the name \'(?P([\w]*))\'" 171 | 172 | def documentation_link(self): 173 | return "https://docs.masoniteproject.com/the-basics/routing#name" 174 | 175 | 176 | class IncludedTemplateNotFound: 177 | def title(self): 178 | return "Check any imported templates inside the :name template." 179 | 180 | def description(self): 181 | return ( 182 | "The :name template was found but a template included inside the :name template was not found. " 183 | "Check any lines of code that use the extends or include Jinja2 tags inside your template. " 184 | "Check the template path is correct. Included templates are absolute from your template directory and should end with '.html'" 185 | ) 186 | 187 | def regex(self): 188 | return ( 189 | r"One of the included templates in the \'(?P([\w]*))\' view could not be found" 190 | ) 191 | 192 | 193 | class UnexpectedEndBlock: 194 | def title(self): 195 | return "Check the :name was closed correctly." 196 | 197 | def description(self): 198 | return "The error could be difficult to find so check ALL :name tags and make sure the :name tag is opened and closed correctly. " 199 | 200 | def regex(self): 201 | return r"Unexpected end of template. Jinja was looking for the following tags: \'(?P([\w]*))\'." 202 | 203 | 204 | class QueryDefaultValue: 205 | def title(self): 206 | return "Missing default value for ':field'" 207 | 208 | def description(self): 209 | return ( 210 | "Default values are typically set on the database level. " 211 | "You can either add a default value on the :field table column in a migration or you should pass a value when creating this record" 212 | ) 213 | 214 | def regex(self): 215 | return r"\(1364\, \"Field \'(?P([\w]*))\' doesn't have a default value\"\)" 216 | 217 | 218 | class NoColumnExistsOnWhere: 219 | def title(self): 220 | return "Check the table for the :field column" 221 | 222 | def description(self): 223 | return "Could not find the :field column. Check your 'where' clauses. Is :field on the table you are trying to query? Did you run the migrations yet? Maybe it was not spelled correctly?" 224 | 225 | def regex(self): 226 | return r"Unknown column \'(?P([\w\.]*))\' in \'where clause\'" 227 | 228 | 229 | class NoRelationExistsInDatabase: 230 | def title(self): 231 | return "The table ':table' does not exist in database" 232 | 233 | def description(self): 234 | return "Could not find the table ':table' in the database. Did you run the migrations yet? Maybe the table was not spelled correctly?" 235 | 236 | def regex(self): 237 | return r"relation \"(?P([\w\.]*))\" does not exist" 238 | 239 | 240 | class NoColumnExistsOnWhereSQLite: 241 | def title(self): 242 | return "Check the table for the :field column" 243 | 244 | def description(self): 245 | return "Could not find the :field column. Is :field on the table you are trying to query? Did you run the migrations yet? Maybe it was not spelled correctly?" 246 | 247 | def regex(self): 248 | return r"no such column: (?P([\w\.]*))" 249 | 250 | 251 | class NoColumnExistsOnSelect: 252 | def title(self): 253 | return "Check the table for the :field column" 254 | 255 | def description(self): 256 | return "Could not find the :field column. Check your 'select' clauses. Is :field on the table you are trying to query? Did you run the migrations yet? Maybe it was not spelled correctly?" 257 | 258 | def regex(self): 259 | return r"Unknown column \'(?P([\w\.]*))\' in \'field list\'" 260 | 261 | 262 | class UnsupportedOperand: 263 | def title(self): 264 | return "Trying to do math for values that are not of the same type (:type1 and :type2)" 265 | 266 | def description(self): 267 | return "Check the type of the 2 types. One is of type :type1 and the the other is of type :type2. They both need to be the same type" 268 | 269 | def regex(self): 270 | return r"unsupported operand type\(s\) for \+\: '(?P([\w\.]*))' and '(?P([\w\.]*))'" 271 | 272 | 273 | class MySQLConnectionRefused: 274 | def title(self): 275 | return "Check database is running and connection details are correct" 276 | 277 | def description(self): 278 | return "Check that MySQL server is running and that MySQL configuration is correct (check that port, hostname, username and password are set correctly, and that environment variables are correctly defined)." 279 | 280 | def regex(self): 281 | return r"Can\'t connect to MySQL server" 282 | 283 | 284 | class PostgresConnectionRefused: 285 | def title(self): 286 | return "Check database is running and connection details are correct" 287 | 288 | def description(self): 289 | return "Check that PostgresSQL server is running and that PostgresSQL configuration is correct (check that port=:port, hostname=:host, username and host are set correctly, and that environment variables are correctly defined)." 290 | 291 | def regex(self): 292 | return r"connection to server at \"(?P([\w\.]*))\", port (?P([\d]*)) failed" 293 | 294 | 295 | class PostgresConnectionFailed: 296 | def title(self): 297 | return "Check database is running and connection details are correct" 298 | 299 | def description(self): 300 | return "Check that PostgresSQL server is running and that PostgresSQL configuration is correct (check that port, hostname, username and host are set correctly, and that environment variables are correctly defined)." 301 | 302 | def regex(self): 303 | return r"connection to server on socket \"(?P(.*))\" failed" 304 | 305 | 306 | class DatabaseDoesNotExist: 307 | def title(self): 308 | return "The database ':database' is not created on the database server" 309 | 310 | def description(self): 311 | return "The application is trying to connect to the database ':database' but it looks like it has not been created." 312 | 313 | def regex(self): 314 | return r"database \"(?P([\w\.]*))\" does not exist" 315 | 316 | 317 | class DatabaseUserDoesNotExist: 318 | def title(self): 319 | return "The database user ':user' is not created on the database server" 320 | 321 | def description(self): 322 | return "The application is trying to connect with the user ':user' but it looks like it has not been created." 323 | 324 | def regex(self): 325 | return r"role \"(?P([\w\.]*))\" does not exist" 326 | 327 | 328 | class DivisionByZeroError: 329 | def title(self): 330 | return "Check variables for any values that could be 0" 331 | 332 | def description(self): 333 | return "Check any place you are doing division. You cannot divide by a zero." 334 | 335 | def regex(self): 336 | return r"division by zero" 337 | 338 | 339 | class ContainerKeyNotFoundRegister: 340 | def title(self): 341 | return "Did you register the key in the service provider or Kernel?" 342 | 343 | def description(self): 344 | return ( 345 | "Check the key name was correctly registered in a service provider or the Kernel file" 346 | ) 347 | 348 | def regex(self): 349 | return r"key was not found in the container" 350 | 351 | 352 | class ContainerKeyNotFoundServiceProvider: 353 | def title(self): 354 | return "Did you register the service provider?" 355 | 356 | def description(self): 357 | return "If you registered the key in your own service provider, did you register the provider in the config/providers.py file?" 358 | 359 | def regex(self): 360 | return r"key was not found in the container" 361 | 362 | def documentation_link(self): 363 | return "https://docs.masoniteproject.com/architecture/service-providers#registering-the-service-provider" 364 | 365 | 366 | class NotFound404: 367 | def title(self): 368 | return "The '/:route' route could not be found" 369 | 370 | def description(self): 371 | return "Could not find the '/:route' route. Try checking spelling is correct and the '/:route' is registered correctly in your routes files. You can also run 'python craft routes:list' to make sure the route shows up correctly" 372 | 373 | def regex(self): 374 | return r"(?P([\w]*)) \: 404 Not Found" 375 | 376 | 377 | class ModelNotFound: 378 | def title(self): 379 | return "No record found when using find_or_fail()" 380 | 381 | def description(self): 382 | return """You probably used 'find_or_fail()' method on a Model, and no record has been found with the given primary key, a ModelNotFound exception has then been raised with a 404 error code. 383 | If you want this error to be silent you can use 'find()' method instead.""" 384 | 385 | def regex(self): 386 | return r"No record found with the given primary key" 387 | 388 | 389 | class DriverNotFound: 390 | def title(self): 391 | return "Driver Is Not Installed" 392 | 393 | def description(self): 394 | return ":package is required by the driver. You should install it with 'pip install :package' and refresh the page." 395 | 396 | def regex(self): 397 | return r"^Could not find the '(?P([\w]*))' library" 398 | 399 | 400 | class MethodNotAllowed: 401 | def title(self): 402 | return "HTTP Method Not Allowed" 403 | 404 | def description(self): 405 | return "You tried to make a :method request on this URL but only :allowed_methods methods are allowed. If you want to use this method, update your routes file else use the allowed methods for making the request to this URL." 406 | 407 | def regex(self): 408 | return r"^(?P([\w]*)) method not allowed for this route. Supported methods are: (?P(\w+\,?\s?)*)." 409 | # return r"^(?P([\w+])) method not allowed for this route. Supported methods are: (?P(\w,+))." 410 | 411 | 412 | class DatabaseDriverNotFound: 413 | def title(self): 414 | return "Database Driver Is Not Installed" 415 | 416 | def description(self): 417 | return ":package is required by the database driver. You should install it with 'pip install :package' and refresh the page." 418 | 419 | def regex(self): 420 | return r"^You must have the '(?P([\w]*))' package installed" 421 | 422 | 423 | class InvalidRouteMethodType: 424 | def title(self): 425 | return "The method type is incorrect" 426 | 427 | def description(self): 428 | return "If this is a GET route, check if the route is actually defined as Route.post(). Or the opposite" 429 | 430 | def regex(self): 431 | return r"(?P([\w]*)) \: 404 Not Found" 432 | 433 | 434 | class GetAttributeObject: 435 | def title(self): 436 | return "Check the class method exists" 437 | 438 | def description(self): 439 | return """ 440 | Double check the object you are using and make sure it has the ':attribute' attribute. 441 | 442 | If you are using a builtin python type then check Python documentation. 443 | If you are using your own class then check the available methods. 444 | """ 445 | 446 | def regex(self): 447 | return r"^'(?P(\w+))' object has no attribute '(?P(\w+))'" 448 | 449 | 450 | class ModuleHasNoAttribute: 451 | def title(self): 452 | return "Check Class Import" 453 | 454 | def description(self): 455 | return """You might have expected to import the class when doing 'from :module import ...' but instead you have imported the module causing this AttributeError exception. 456 | 457 | Please check that the python module exports the class you want (e.g. through a __init__.py file) else you can write the import "from my.module.MyClass import MyClass" 458 | """ 459 | 460 | def regex(self): 461 | return r"^module '(?P(\w.+))' has no attribute 'find_or_fail'" 462 | 463 | 464 | class ModuleNotCallable: 465 | def title(self): 466 | return "Check Class Import" 467 | 468 | def description(self): 469 | return """You might have expected to import the class when doing 'from :module import ...' but instead you have imported the module causing this TypeError exception when trying to instantiate the class. 470 | 471 | Please check that the python module exports the class you want (e.g. through a __init__.py file) else you can write the import "from my.module.MyClass import MyClass" 472 | """ 473 | 474 | def regex(self): 475 | return r"'module' object is not callable" 476 | 477 | 478 | class NoModuleNamed: 479 | def title(self): 480 | return "Module Not Found Error" 481 | 482 | def description(self): 483 | return "This is an import error. Check the file where you imported the ': module' module. Make sure its spelled right and make sure you pip installed this module correctly if this is supposed to come from a PYPI package." 484 | 485 | def regex(self): 486 | return r"No module named '(?P(\w.+))'" 487 | 488 | 489 | class Syntax: 490 | def title(self): 491 | return "Syntax Error" 492 | 493 | def description(self): 494 | return "Syntax errors are usually simple to fix. Just find the place that has invalid Python syntax and fix it. A good place to look is in your :class file on line :line" 495 | 496 | def regex(self): 497 | return r"^invalid syntax \((?P(\w+\.py))+, line (?P(\w+))" 498 | 499 | 500 | class ImportIssue: 501 | def title(self): 502 | return "Import Issue" 503 | 504 | def description(self): 505 | return "This is an import error. Check the file where you imported the ':object' class and make sure it exists there." 506 | 507 | def regex(self): 508 | return r"^cannot import name '(?P(\w+))'" 509 | 510 | 511 | class Undefined: 512 | def title(self): 513 | return "Undefined Variable" 514 | 515 | def description(self): 516 | return "You are trying to use a variable that cannot be found. Check the ':variable' variable and see if it is declared, imported or in the correct scope depending on what the variable is." 517 | 518 | def regex(self): 519 | return r"name '(?P(\w+))' is not defined" 520 | 521 | 522 | class WrongParameterCount: 523 | def title(self): 524 | return "Wrong Parameter Count" 525 | 526 | def description(self): 527 | return ( 528 | "You have the wrong amount of parameters for the ':object' object. " 529 | "It requires :correct parameters but you gave :wrong parameters. If the parameters are stored in a variable try checking the variable to the left. " 530 | "If you are passing variables in normally then check the signature of the object" 531 | ) 532 | 533 | def regex(self): 534 | return r"^(?P(\w*))\(\) takes (?P(\d+)) positional (argument|arguments) but (?P(\d+)) (were|was) given" 535 | 536 | 537 | class WrongConstructorParameterCount: 538 | def title(self): 539 | return "Wrong Parameters to a Constructor" 540 | 541 | def description(self): 542 | return ( 543 | "The ':object' object doesn't take parameters but you gave some anyway. " 544 | "Check the constructor of the ':object' object. It's likely it does not take any parameters. " 545 | "If its stored in a variable you can check the value to the left." 546 | ) 547 | 548 | def regex(self): 549 | return r"^(?P(\w*))\(\) takes no parameters " 550 | 551 | 552 | class ObjectNotCallable: 553 | def title(self): 554 | return "Objects Cannot Be Called" 555 | 556 | def description(self): 557 | return ( 558 | "You cannot call objects. The ':object' object has already been instantiated. " 559 | "Once an object is instantiated it cannot be called directly anymore. " 560 | "Check if the ':object' is instantiated already." 561 | ) 562 | 563 | def regex(self): 564 | return r"^'(?P(\w*))' object is not callable" 565 | 566 | 567 | class SubscriptableIssue: 568 | def title(self): 569 | return "Object Not Subscriptable" 570 | 571 | def description(self): 572 | return "Looks like you expected ':object' to be an iterable but it is not. You can only use subscriptions, like x[0], on iterable type objects (like lists, dicts, and strings) but not ':object' in this case." 573 | 574 | def regex(self): 575 | return r"^'(?P(\w+))' object is not subscriptable" 576 | 577 | 578 | class StringIndicesMustBeIntegers: 579 | def title(self): 580 | return "Check Variable Type" 581 | 582 | def description(self): 583 | return ( 584 | "This errors might occur when using subscriptions [] on an object. The most likely cause is that you expected the object to be a dict and wanted to access a key on it. " 585 | "Let's take the example of accessing 'some_key' on the dict named `my_var`: my_var['some_key'] will fail if 'my_var' happens to be a string ! You cannot access 'some_key' on it, you can only access indexes of the string with integers. " 586 | "Check that the variable is a dictionary and not a string." 587 | ) 588 | 589 | def regex(self): 590 | return r"string indices must be integers" 591 | 592 | 593 | class DjangoTemplateNotFound: 594 | def title(self): 595 | return "Check template exists in your apps 'templates' directory" 596 | 597 | def description(self): 598 | return "Check for a ':path' file inside the 'templates' directory of your app." 599 | 600 | def regex(self): 601 | return r"^(^(?P(^(.+)\/([^\/]+)$)))" 602 | 603 | 604 | class ContextShouldBeList: 605 | def title(self): 606 | return "Check the arguments to the template loader method." 607 | 608 | def description(self): 609 | return "Change the argument passed to the template loader from a list to a dictionary" 610 | 611 | def regex(self): 612 | return r"^context must be a dict rather than list" 613 | 614 | 615 | class RenderArgumentsOutOfOrder: 616 | def title(self): 617 | return "Check the arguments to the template render method." 618 | 619 | def description(self): 620 | return "If trying to load a template, check the order of the arguments. The order should be (request, template, context). Context here is a dictionary." 621 | 622 | def regex(self): 623 | return r"^join\(\) argument must be str\, bytes\, or os.PathLike object\, not \'dict\'" 624 | -------------------------------------------------------------------------------- /src/exceptionite/tabs/ContextTab.py: -------------------------------------------------------------------------------- 1 | from ..Tab import Tab 2 | from ..blocks.Environment import Environment 3 | from ..blocks.Packages import Packages 4 | from ..blocks.Git import Git 5 | 6 | 7 | class ContextTab(Tab): 8 | 9 | name = "Context" 10 | id = "context" 11 | icon = "ViewListIcon" 12 | 13 | def __init__(self, handler): 14 | super().__init__(handler) 15 | self.add_blocks(Environment, Packages, Git) 16 | -------------------------------------------------------------------------------- /src/exceptionite/tabs/RecommendationsTab.py: -------------------------------------------------------------------------------- 1 | from ..Tab import Tab 2 | from ..blocks.PackagesUpdates import PackagesUpdates 3 | 4 | 5 | class RecommendationsTab(Tab): 6 | 7 | name = "Recommendations" 8 | id = "recommendations" 9 | icon = "CheckCircleIcon" 10 | advertise_content = True 11 | 12 | def __init__(self, handler): 13 | super().__init__(handler) 14 | self.add_blocks(PackagesUpdates) 15 | -------------------------------------------------------------------------------- /src/exceptionite/tabs/SolutionsTab.py: -------------------------------------------------------------------------------- 1 | from ..Tab import Tab 2 | from ..blocks.StackOverflow import StackOverflow 3 | from ..blocks.PossibleSolutions import PossibleSolutions 4 | 5 | 6 | class SolutionsTab(Tab): 7 | 8 | name = "Solutions" 9 | id = "solutions" 10 | icon = "LightBulbIcon" 11 | advertise_content = True 12 | 13 | def __init__(self, handler): 14 | super().__init__(handler) 15 | self.add_blocks(PossibleSolutions, StackOverflow) 16 | -------------------------------------------------------------------------------- /src/exceptionite/tabs/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: E501 2 | from .ContextTab import ContextTab 3 | from .SolutionsTab import SolutionsTab 4 | from .RecommendationsTab import RecommendationsTab 5 | -------------------------------------------------------------------------------- /src/exceptionite/templates/exception.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Error - {{ data.exception.message }} 8 | 11 | 12 | 13 | 14 |
15 | 18 | 21 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /src/exceptionite/templates/exceptionite.css: -------------------------------------------------------------------------------- 1 | .dark .hljs{color:#abb2bf}.dark .hljs-comment,.dark .hljs-quote{color:#5c6370;font-style:italic}.dark .hljs-doctag,.dark .hljs-formula,.dark .hljs-keyword{color:#c678dd}.dark .hljs-deletion,.dark .hljs-name,.dark .hljs-section,.dark .hljs-selector-tag,.dark .hljs-subst{color:#e06c75}.dark .hljs-literal{color:#56b6c2}.dark .hljs-addition,.dark .hljs-attribute,.dark .hljs-meta .hljs-string,.dark .hljs-regexp,.dark .hljs-string{color:#98c379}.dark .hljs-attr,.dark .hljs-number,.dark .hljs-selector-attr,.dark .hljs-selector-class,.dark .hljs-selector-pseudo,.dark .hljs-template-variable,.dark .hljs-type,.dark .hljs-variable{color:#d19a66}.dark .hljs-bullet,.dark .hljs-link,.dark .hljs-meta,.dark .hljs-selector-id,.dark .hljs-symbol,.dark .hljs-title{color:#61aeee}.dark .hljs-built_in,.dark .hljs-class .hljs-title,.dark .hljs-title.class_{color:#e6c07b}.dark .hljs-emphasis{font-style:italic}.dark .hljs-strong{font-weight:700}.dark .hljs-link{text-decoration:underline} 2 | .hljs{background:#fafafa;color:#383a42}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} 3 | /*! tailwindcss v3.0.16 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}:root{--stack-height:var(--tab-main-height)}.stack{display:grid;grid-template:calc(var(--stack-height)*.4) calc(var(--stack-height)*.6) /1fr}@media (min-width:640px){.stack{align-items:stretch;grid-template:var(--stack-height) /22rem 1fr}}.stack-nav{--tw-border-opacity:1;border-bottom-width:1px;border-color:rgb(209 213 219/var(--tw-border-opacity));height:100%}.dark .stack-nav{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.stack-nav{display:grid;font-size:.75rem;grid-template:1fr/100%;line-height:1rem;overflow:hidden}@media (min-width:640px){.stack-nav{border-bottom-width:0;border-right-width:1px;display:grid;grid-template:auto 1fr/100%}}.stack-nav-actions{display:none}.stack-nav-arrows{align-items:center;display:grid;gap:.25rem;justify-content:center;padding-left:.75rem;padding-right:.75rem;width:2.5rem}.stack-nav-arrow{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity));font-size:.75rem;line-height:1rem}.stack-nav-arrow:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.stack-frames{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity));border-top-width:1px;overflow:hidden}.stack-frames-scroll{bottom:0;left:0;overflow-x:hidden;overflow-y:auto;position:absolute;right:0;top:0}.stack-frame-group{--tw-border-opacity:1;border-bottom-width:1px;border-color:rgb(209 213 219/var(--tw-border-opacity))}.stack-frame{align-items:flex-end;display:grid;grid-template-columns:2rem auto auto}@media (min-width:640px){.stack-frame{grid-template-columns:3rem 1fr auto}}.stack-frame:not(:first-child){margin-top:-.5rem}.stack-frame-selected,.stack-frame-selected .stack-frame-header{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity));z-index:10}.stack-frame-number{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity));padding:1rem .5rem;text-align:center}.stack-frame-header{margin-right:-2.5rem;width:100%}.stack-frame-text{--tw-border-opacity:1;--tw-text-opacity:1;align-items:center;border-color:rgb(216 180 254/var(--tw-border-opacity));border-left-width:2px;color:rgb(55 65 81/var(--tw-text-opacity));display:grid;gap:.5rem;padding-bottom:1rem;padding-left:.75rem;padding-top:1rem}.stack-frame-group-vendor .stack-frame-text{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.stack-frame-selected .stack-frame-text{--tw-border-opacity:1;border-color:rgb(168 85 247/var(--tw-border-opacity))}.stack-frame-group-vendor .stack-frame-selected .stack-frame-text{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.stack-frame-line{line-height:1.25;padding:1rem .25rem 1rem .5rem;text-align:right}.stack-main{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity));display:grid;grid-template:auto 1fr/100%;height:100%;overflow:hidden}.stack-main-header{--tw-border-opacity:1;border-bottom-width:1px;border-color:rgb(229 231 235/var(--tw-border-opacity));font-size:.75rem;line-height:1rem;padding:.5rem 1.5rem}@media (min-width:640px){.stack-main-header{font-size:1rem;line-height:1.5rem;padding-bottom:1rem;padding-top:1rem}}.stack-main-content{overflow:hidden}.stack-viewer{bottom:0;display:flex;font-size:.75rem;left:0;line-height:1rem;overflow:auto;right:0;top:0}.stack-ruler{position:-webkit-sticky;position:sticky}.dark .stack-ruler{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.stack-ruler{align-self:stretch;flex:none;left:0;z-index:20}.stack-lines{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity));border-right-width:1px;min-height:100%}.dark .stack-lines{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.stack-lines{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.dark .stack-lines{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.stack-lines{padding-bottom:2rem;padding-top:2rem}.stack-line,.stack-lines{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.stack-line{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity));font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;line-height:2;padding-left:.5rem;padding-right:.5rem}.dark .stack-line{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.stack-line-highlight{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity));color:rgb(37 99 235/var(--tw-text-opacity))}.dark .stack-line-highlight{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity));color:rgb(220 38 38/var(--tw-text-opacity))}.stack-code{flex-grow:1;padding-bottom:1rem;padding-top:1rem}.stack-code-line{line-height:2;padding-left:.75rem}.stack-code-line:hover{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.dark .stack-code-line:hover{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.stack-code-line .editor-link{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity));display:inline-block;opacity:0;padding-left:.5rem;padding-right:.5rem}.stack-code-line-highlight{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.dark .stack-code-line-highlight{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.btn{align-items:center;border-color:rgb(209 213 219/var(--tw-border-opacity));border-radius:.375rem;border-width:1px;color:rgb(75 85 99/var(--tw-text-opacity));display:inline-flex;font-size:.75rem;font-weight:500;line-height:1rem;padding:.5rem .75rem}.btn,.btn:hover{--tw-border-opacity:1;--tw-text-opacity:1}.btn:hover{border-color:rgb(59 130 246/var(--tw-border-opacity));color:rgb(59 130 246/var(--tw-text-opacity))}.btn:focus{outline:2px solid transparent;outline-offset:2px}.dark .btn{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity));border-color:rgb(156 163 175/var(--tw-border-opacity));color:rgb(156 163 175/var(--tw-text-opacity))}.dark .btn:hover{--tw-border-opacity:1;--tw-text-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity));color:rgb(229 231 235/var(--tw-text-opacity))}.btn-disabled{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity));cursor:not-allowed}.btn-disabled:hover{background-color:transparent}.dark .btn-disabled{--tw-text-opacity:1}.dark .btn-disabled,.dark .btn-disabled:hover{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity));color:rgb(55 65 81/var(--tw-text-opacity))}.dark .btn-disabled:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.btn-solution{--tw-text-opacity:1;background-color:transparent}.btn-solution,.btn-solution:hover{--tw-border-opacity:1;border-color:rgb(21 128 61/var(--tw-border-opacity));color:rgb(21 128 61/var(--tw-text-opacity))}.btn-solution:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(134 239 172/var(--tw-bg-opacity))}.dark .btn-solution{--tw-border-opacity:1;--tw-text-opacity:1;background-color:transparent;border-color:rgb(255 255 255/var(--tw-border-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.dark .btn-solution:hover{--tw-bg-opacity:1;background-color:rgb(30 58 138/var(--tw-bg-opacity))}.btn-no-solution{--tw-text-opacity:1;background-color:transparent}.btn-no-solution,.btn-no-solution:hover{--tw-border-opacity:1;border-color:rgb(161 98 7/var(--tw-border-opacity));color:rgb(161 98 7/var(--tw-text-opacity))}.btn-no-solution:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.dark .btn-no-solution{--tw-border-opacity:1;--tw-text-opacity:1;background-color:transparent;border-color:rgb(113 63 18/var(--tw-border-opacity));color:rgb(113 63 18/var(--tw-text-opacity))}.dark .btn-no-solution:hover{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.definition-list{-moz-column-gap:1.5rem;column-gap:1.5rem;display:grid;row-gap:.5rem}.definition-list .definition-list{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity));border-left-width:2px;padding-left:1rem}@media (min-width:640px){.definition-list{grid-template-columns:8rem 1fr}.definition-list .definition-list{grid-template-columns:auto 1fr}}@media (min-width:1024px){.definition-list{grid-template-columns:14rem 1fr}}.definition-list-title{font-weight:600;margin-bottom:.75rem}@media (min-width:640px){.definition-list-title{margin-left:9.5rem}}@media (min-width:1024px){.definition-list-title{margin-left:15.5rem}}.definition-label{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity));line-height:1.25;overflow-wrap:break-word}@media (min-width:640px){.definition-label{text-align:right}}.definition-value{line-height:1.25;margin-bottom:1rem;word-break:break-all}@media (min-width:640px){.definition-value{margin-bottom:0}}.definition-label:empty:after,.definition-value:empty:after{content:"—"}.definition-label:empty:after,.definition-list-empty,.definition-value:empty:after{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}@media (min-width:640px){.definition-list-empty{padding-left:.5rem}.definition-list .definition-list .definition-list-empty{padding-left:.25rem}}.navbar-link{--tw-text-opacity:1;align-items:center;border-radius:.375rem;color:rgb(17 24 39/var(--tw-text-opacity));cursor:pointer;display:inline-flex;font-size:.875rem;font-weight:500;line-height:1.25rem;padding:.5rem .75rem}.navbar-link:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.dark .navbar-link{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .navbar-link:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.navbar-link-disabled{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity));cursor:not-allowed}.dark .navbar-link-disabled{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .navbar-link-disabled:hover{background-color:transparent}.navbar-link-icon{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity));height:1.25rem;width:1.25rem}.group:hover .navbar-link-icon{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.navbar-link-disabled .navbar-link-icon{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark .navbar-link-disabled .navbar-link-icon{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.navbar-link-icon-only{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity));height:1.25rem;width:1.25rem}.group:hover .navbar-link-icon-only{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.dark .navbar-link-icon-only{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.navbar-link-disabled .navbar-link-icon-only{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.dark .navbar-link-disabled .navbar-link-icon-only{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.badge{align-items:center;border-radius:.25rem;display:inline-flex;font-size:.75rem;font-weight:500;line-height:1rem;padding:.25rem .5rem}.badge-gray{background-color:rgb(243 244 246/var(--tw-bg-opacity));color:rgb(31 41 55/var(--tw-text-opacity))}.badge-gray,.dark .badge-gray{--tw-bg-opacity:1;--tw-text-opacity:1}.dark .badge-gray{background-color:rgb(17 24 39/var(--tw-bg-opacity));color:rgb(156 163 175/var(--tw-text-opacity))}.badge-indigo{background-color:rgb(199 210 254/var(--tw-bg-opacity));color:rgb(55 48 163/var(--tw-text-opacity))}.badge-indigo,.dark .badge-indigo{--tw-bg-opacity:1;--tw-text-opacity:1}.dark .badge-indigo{background-color:rgb(55 48 163/var(--tw-bg-opacity));color:rgb(199 210 254/var(--tw-text-opacity))}.badge-blue{background-color:rgb(199 210 254/var(--tw-bg-opacity));color:rgb(30 64 175/var(--tw-text-opacity))}.badge-blue,.dark .badge-blue{--tw-bg-opacity:1;--tw-text-opacity:1}.dark .badge-blue{background-color:rgb(30 64 175/var(--tw-bg-opacity));color:rgb(191 219 254/var(--tw-text-opacity))}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{bottom:0;left:0;right:0;top:0}.top-14{top:3.5rem}.top-0{top:0}.right-0{right:0}.top-28{top:7rem}.top-2{top:.5rem}.right-2{right:.5rem}.bottom-2{bottom:.5rem}.top-1{top:.25rem}.-top-0\.5{top:-.125rem}.-right-0\.5{right:-.125rem}.-top-0{top:0}.-right-0{right:0}.z-30{z-index:30}.z-40{z-index:40}.col-span-1{grid-column:span 1/span 1}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-2{grid-column:span 2/span 2}.col-span-10{grid-column:span 10/span 10}.mx-auto{margin-left:auto;margin-right:auto}.my-8{margin-bottom:2rem;margin-top:2rem}.-mx-1\.5{margin-left:-.375rem;margin-right:-.375rem}.-my-1\.5{margin-bottom:-.375rem;margin-top:-.375rem}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-my-1{margin-bottom:-.25rem;margin-top:-.25rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.mt-4{margin-top:1rem}.mb-10{margin-bottom:2.5rem}.mb-4{margin-bottom:1rem}.mr-1{margin-right:.25rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mb-8{margin-bottom:2rem}.mb-3{margin-bottom:.75rem}.mr-2{margin-right:.5rem}.ml-6{margin-left:1.5rem}.mt-2{margin-top:.5rem}.-ml-0\.5{margin-left:-.125rem}.-ml-0{margin-left:0}.ml-auto{margin-left:auto}.ml-4{margin-left:1rem}.ml-3{margin-left:.75rem}.mt-3{margin-top:.75rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.block{display:block}.\!block{display:block!important}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-full{height:100%}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-6{height:1.5rem}.min-h-screen{min-height:100vh}.w-full{width:100%}.w-4{width:1rem}.w-5{width:1.25rem}.w-72{width:18rem}.w-2{width:.5rem}.w-11{width:2.75rem}.max-w-4xl{max-width:56rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-center{transform-origin:center}.origin-top-right{transform-origin:top right}.translate-y-1\/2{--tw-translate-y:50%}.translate-x-6,.translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-6{--tw-translate-x:1.5rem}.translate-x-1{--tw-translate-x:0.25rem}.transform,.translate-x-1{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}.animate-ping{-webkit-animation:ping 1s cubic-bezier(0,0,.2,1) infinite;animation:ping 1s cubic-bezier(0,0,.2,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-y-2{row-gap:.5rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.self-center{align-self:center}.self-stretch{align-self:stretch}.self-baseline{align-self:baseline}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.rounded-md{border-radius:.375rem}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-sm{border-radius:.125rem}.rounded-full{border-radius:9999px}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-red-800{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.bg-yellow-800{--tw-bg-opacity:1;background-color:rgb(133 77 14/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(22 101 52/var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(30 64 175/var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.bg-indigo-800{--tw-bg-opacity:1;background-color:rgb(55 48 163/var(--tw-bg-opacity))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity))}.bg-purple-800{--tw-bg-opacity:1;background-color:rgb(107 33 168/var(--tw-bg-opacity))}.bg-pink-100{--tw-bg-opacity:1;background-color:rgb(252 231 243/var(--tw-bg-opacity))}.bg-pink-800{--tw-bg-opacity:1;background-color:rgb(157 23 77/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-8{padding:2rem}.p-1\.5{padding:.375rem}.p-1{padding:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.pt-4{padding-top:1rem}.pr-4{padding-right:1rem}.pr-3{padding-right:.75rem}.pr-8{padding-right:2rem}.pl-3{padding-left:.75rem}.pr-0{padding-right:0}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-2xl{font-size:1.5rem;line-height:2rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity))}.text-green-700{--tw-text-opacity:1;color:rgb(21 128 61/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.opacity-30{opacity:.3}.opacity-75{opacity:.75}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:bg-yellow-100:hover{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:text-yellow-500{--tw-text-opacity:1;color:rgb(234 179 8/var(--tw-text-opacity))}.group:hover .group-hover\:text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.dark .dark\:divide-gray-400>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(156 163 175/var(--tw-divide-opacity))}.dark .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-blue-800{--tw-bg-opacity:1;background-color:rgb(30 64 175/var(--tw-bg-opacity))}.dark .dark\:bg-yellow-600{--tw-bg-opacity:1;background-color:rgb(202 138 4/var(--tw-bg-opacity))}.dark .dark\:bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.dark .dark\:bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.dark .dark\:bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.dark .dark\:text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.dark .dark\:text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.dark .dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark .dark\:ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.dark .dark\:ring-inset{--tw-ring-inset:inset}.dark .dark\:ring-white\/10{--tw-ring-color:hsla(0,0%,100%,.1)}.dark .dark\:hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:hover\:bg-yellow-700:hover{--tw-bg-opacity:1;background-color:rgb(161 98 7/var(--tw-bg-opacity))}.dark .dark\:hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.dark .dark\:hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark .dark\:hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}@media (min-width:768px){.md\:inline{display:inline}.md\:flex{display:flex}.md\:justify-between{justify-content:space-between}}@media (min-width:1024px){.lg\:p-4{padding:1rem}.lg\:px-4{padding-left:1rem;padding-right:1rem}} 4 | -------------------------------------------------------------------------------- /src/exceptionite/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.2.5" 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "src/exceptionite/assets/**/*.vue", 4 | "src/exceptionite/assets/**/*.js", 5 | "src/exceptionite/templates/exception.html" 6 | ], 7 | safelist: [ 8 | { 9 | pattern: /bg-(gray|red|yellow|green|blue|indigo|purple|pink)-(100|800)/, 10 | }, 11 | ], 12 | darkMode: 'class', 13 | theme: { 14 | extend: {}, 15 | }, 16 | plugins: [], 17 | } 18 | -------------------------------------------------------------------------------- /tests/test_handler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from src.exceptionite import Handler, Tab 4 | from src.exceptionite.exceptions import ConfigurationException 5 | 6 | 7 | class OtherContextTab(Tab): 8 | id = "context" 9 | name = "An other context tab" 10 | 11 | 12 | class TestHandler(unittest.TestCase): 13 | def setUp(self) -> None: 14 | super().setUp() 15 | self.handler = Handler() 16 | 17 | def test_handler_can_provide_basic_exception_data(self): 18 | try: 19 | raise ValueError("Custom message") 20 | except Exception as exception: 21 | self.handler.start(exception) 22 | 23 | assert self.handler.message() == "Custom message" 24 | assert self.handler.exception() == "ValueError" 25 | assert self.handler.namespace() == "builtins.ValueError" 26 | assert self.handler.count() > 0 27 | 28 | frame = self.handler.stacktrace()[0] 29 | assert frame.index == 0 30 | assert frame.relative_file == "tests/test_handler.py" 31 | assert not frame.is_vendor 32 | assert frame.lineno == 19 33 | assert frame.offending_line == 19 34 | assert frame.method == "test_handler_can_provide_basic_exception_data" 35 | 36 | def test_cannot_override_tab_context(self): 37 | with self.assertRaises(ConfigurationException): 38 | self.handler.renderer("web").add_tabs(OtherContextTab) 39 | -------------------------------------------------------------------------------- /tests/test_json_renderer.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/tests/test_json_renderer.py -------------------------------------------------------------------------------- /tests/test_terminal_renderer.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasoniteFramework/exceptionite/f9ecc09522e59740038acbbe94e5c2a3509d610c/tests/test_terminal_renderer.py -------------------------------------------------------------------------------- /tests/test_web_renderer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from dotty_dict import dotty 3 | 4 | from src.exceptionite import Handler, Tab 5 | 6 | 7 | class CustomTestTab(Tab): 8 | id = "test" 9 | name = "Test tab" 10 | 11 | def build(self): 12 | return {"key": "value"} 13 | 14 | 15 | class TestWebRenderer(unittest.TestCase): 16 | def setUp(self) -> None: 17 | super().setUp() 18 | self.handler = Handler() 19 | try: 20 | raise ValueError("Custom message") 21 | except Exception as exception: 22 | self.handler.start(exception) 23 | 24 | def test_enabled_tabs(self): 25 | web = self.handler.renderer("web") 26 | assert len(web.enabled_tabs()) == 3 27 | 28 | def test_build_page_context(self): 29 | context = dotty(self.handler.renderer("web").build()) 30 | assert context.get("exception.type") == "ValueError" 31 | assert context.get("exception.namespace") == "builtins.ValueError" 32 | assert context.get("exception.message") == "Custom message" 33 | assert context.get("config") 34 | assert context.get("actions") == [] 35 | context_tab = context.get("tabs")[0] 36 | assert context_tab.get("id") == "context" 37 | assert context_tab.get("name") == "Context" 38 | assert context_tab.get("has_content") 39 | assert not context_tab.get("has_sections") 40 | blocks = context_tab.get("blocks") 41 | assert len(blocks) == 3 42 | env_block = blocks[0] 43 | assert env_block.get("id") == "environment" 44 | assert env_block.get("name") == "System Environment" 45 | assert env_block.get("has_content") 46 | assert env_block.get("icon") == "TerminalIcon" 47 | assert env_block.get("data").get("Arch") == "64bit" 48 | 49 | def test_add_tabs(self): 50 | web = self.handler.renderer("web") 51 | web.add_tabs(CustomTestTab) 52 | assert len(web.enabled_tabs()) == 4 53 | 54 | def test_add_actions(self): 55 | pass 56 | 57 | def test_can_display_error_page(self): 58 | exception = ValueError("Custom message") 59 | self.handler.start(exception) 60 | 61 | assert "Custom message" in self.handler.render("web") 62 | assert "ValueError" in self.handler.render("web") 63 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix') 2 | const path = require("path") 3 | 4 | mix 5 | .setPublicPath("src/exceptionite/templates") 6 | .js('src/exceptionite/assets/app.js', 'exceptionite.js').vue() 7 | .postCss("src/exceptionite/assets/app.css", "exceptionite.css", 8 | [require("tailwindcss"),] 9 | ) 10 | 11 | mix.alias({ 12 | "@": path.resolve("src/exceptionite/assets"), 13 | }) 14 | 15 | mix.disableSuccessNotifications() --------------------------------------------------------------------------------